Compare commits

..

138 Commits

Author SHA1 Message Date
Barış Soner Uşaklı
8bc8cf1ba0 lint 2023-05-15 12:15:48 -04:00
Barış Soner Uşaklı
62e162cf1e fix: backport ws token fix 2023-05-15 11:55:18 -04:00
psibean
a5d92da9dd Replace csurf with csrf-sync 2023-05-15 11:48:25 -04:00
Julian Lam
2bd6eea2fa fix: #11554, email requirement bypass by sending in whitespace 2023-05-02 12:01:28 -04:00
Misty Release Bot
42b9fbc91c chore: incrementing version number - v2.8.12
(cherry picked from commit 3e494a1ea0)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2023-04-26 14:34:45 +00:00
Misty Release Bot
5c0bf7ccbe Merge commit '30b6bcfca117e667c262c0462fc5f0100e6a436c' into v2.x 2023-04-26 14:34:42 +00:00
Barış Soner Uşaklı
30b6bcfca1 fix: #11519, clear parent cache all the way to root 2023-04-26 10:13:27 -04:00
Barış Soner Uşaklı
de2669a2c6 fix: only remove deleted tag, closes #11515 2023-04-24 15:39:00 -04:00
Barış Soner Uşaklı
21fb8590e5 test: remove old comment 2023-04-23 18:59:18 -04:00
Veronikya
c931183287 fix: NodeBB#11482 thumbs Post Can not upload a thumbnail, only multip… (#11483)
* fix: NodeBB#11482 thumbs Post Can not upload a thumbnail, only multiple uploads

* Modify upload thumbnail test

* Modify upload thumbnail test +,

* Get rid of v2 uploads test

* edit times

* Modify amount of files associated post test

* edit post file amount
2023-04-15 17:56:36 -04:00
Brutus5000
ae5afdbc66 feat: name theme on error:theme-not-set-in-configuration 2023-04-11 13:42:21 -04:00
Julian Lam
5343d2a01b chore: removing superfluous changelog items 2023-04-10 21:51:12 -04:00
Misty Release Bot
2ec81eff43 chore: incrementing version number - v2.8.11
(cherry picked from commit 82f0efb14b)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2023-04-11 01:49:11 +00:00
Misty Release Bot
f2ca93f6c6 chore: update changelog for v2.8.11 2023-04-11 01:49:11 +00:00
Misty Release Bot
82f0efb14b chore: incrementing version number - v2.8.11 2023-04-11 01:49:11 +00:00
Misty Release Bot
df08b47163 Merge commit 'c27567289f9937abd4abe6960a9b6e387cf68331' into v2.x 2023-04-11 01:49:09 +00:00
Opliko
c27567289f ci: publish to ghcr instead of docker hub 2023-04-05 14:38:47 -04:00
Julian Lam
c33730530e Revert "docs: update readme with new screenshot and updated copy for Harmony"
This reverts commit 67055006df.
2023-03-29 10:32:05 -04:00
Julian Lam
67055006df docs: update readme with new screenshot and updated copy for Harmony 2023-03-29 10:31:39 -04:00
Barış Soner Uşaklı
e0b2065802 test: update socket.io test 2023-03-28 08:15:42 -04:00
Barış Soner Uşaklı
4d2d76897a fix: don't crash on objects with toString property 2023-03-28 08:08:59 -04:00
Barış Soner Uşaklı
7397873db3 fix: fire action:user.online on user login 2023-03-27 22:16:41 -04:00
Misty Release Bot
5b7c3671c8 chore: incrementing version number - v2.8.10
(cherry picked from commit 48c1c7594d)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2023-03-27 18:10:57 +00:00
Misty Release Bot
188ec62f9a chore: update changelog for v2.8.10 2023-03-27 18:10:57 +00:00
Misty Release Bot
48c1c7594d chore: incrementing version number - v2.8.10 2023-03-27 18:10:57 +00:00
Misty Release Bot
73ff25887c Merge commit '830f142b7aea2e597294a84d52c05aab3a3539ca' into v2.x 2023-03-27 15:12:54 +00:00
Julian Lam
830f142b7a fix: #11403, remove loader.js crash counter logic 2023-03-27 10:55:44 -04:00
Barış Soner Uşaklı
1aff9cad91 lint: fix arrow 2023-03-27 10:47:15 -04:00
Barış Soner Uşaklı
37b48b82a4 fix: don't crash if event name is not a string 2023-03-27 10:38:53 -04:00
Barış Soner Uşaklı
e9a8e19508 chore: up composer-default 2023-03-21 10:13:21 -04:00
Barış Soner Uşaklı
894f392bfc lint: whitespace 2023-03-20 11:17:05 -04:00
Barış Soner Uşaklı
c2961ad4cd fix: closes #11173, move cache clear code
if 2 deps were updated only one of them was cleared from require.cache. ie commander & lru-cache both has major version bump then only commander would be cleared from cache since it throws first
2023-03-20 11:05:48 -04:00
Misty Release Bot
57f14e419f chore: incrementing version number - v2.8.9
(cherry picked from commit fb100ac731)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2023-03-19 16:31:26 +00:00
Misty Release Bot
18b2150edd chore: update changelog for v2.8.9 2023-03-19 16:31:26 +00:00
Misty Release Bot
fb100ac731 chore: incrementing version number - v2.8.9 2023-03-19 16:31:26 +00:00
Misty Release Bot
bb725987b3 Merge commit '73a50d17180dcd6cb42ef9cf305a480f92b4af05' into v2.x 2023-03-19 16:31:24 +00:00
Barış Soner Uşaklı
73a50d1718 chore: up cron 2023-03-19 12:21:06 -04:00
Julian Lam
93aa43f717 style: more fixes 2023-03-17 15:48:44 -04:00
Phạm Tấn Minh Tiến
9ed6961af8 fix lint 2023-03-17 15:48:44 -04:00
Phạm Tấn Minh Tiến
4b94c033c4 wrap quotes to prevent stripping leading 0 2023-03-17 15:48:44 -04:00
Barış Soner Uşaklı
9e685e657a test: openapi for thumbs 2023-03-15 15:18:05 -04:00
Barış Soner Uşaklı
767c1d1faf fix: thumb remove on windows, closes #11357 2023-03-14 15:09:12 -04:00
Barış Soner Uşaklı
a3a38e4ba3 fix: #11357 clear cache on thumb remove 2023-03-14 14:30:46 -04:00
Barış Soner Uşaklı
cfd5027245 fix: closes #11352, try/catch rss feeds 2023-03-11 16:07:02 -05:00
Barış Soner Uşaklı
56427e4f9d fix: closes #11343, don't crash if tags array is empty 2023-03-10 11:40:02 -05:00
Misty Release Bot
b331b9423b chore: incrementing version number - v2.8.8
(cherry picked from commit f5a59991fc)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2023-03-09 14:58:29 +00:00
Misty Release Bot
c03d5db71e chore: update changelog for v2.8.8 2023-03-09 14:58:28 +00:00
Misty Release Bot
f5a59991fc chore: incrementing version number - v2.8.8 2023-03-09 14:58:28 +00:00
Misty Release Bot
e45a6de24b Merge commit '22fc8fe38fd3b3c8ba6300ca6d12d90eb9b990ca' into v2.x 2023-03-09 14:58:22 +00:00
Julian Lam
22fc8fe38f fix: stop topic navigation hotkeys from firing if in a mousetrap-enabled form element 2023-03-09 09:55:32 -05:00
Julian Lam
17d0b40efa fix: stop topic navigation hotkeys from firing if in a mousetrap-enabled form element 2023-03-09 09:44:06 -05:00
Barış Soner Uşaklı
1545223e7f fix: tag filtering when changing filter to watched topics
or changing popular time limit to month
2023-03-08 18:28:55 -05:00
Barış Soner Uşaklı
f054a4f44d fix: get cid from pid instead of passing in 2023-03-06 09:44:21 -05:00
Barış Soner Uşaklı
8c762d3228 fix: closes #11331, allow 0 length content if set to 0 in acp 2023-03-03 09:30:51 -05:00
Misty Release Bot
3f8248d673 chore: incrementing version number - v2.8.7
(cherry picked from commit 6976925943)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2023-03-01 15:51:30 +00:00
Misty Release Bot
2ca38e7b95 chore: update changelog for v2.8.7 2023-03-01 15:51:30 +00:00
Misty Release Bot
6976925943 chore: incrementing version number - v2.8.7 2023-03-01 15:51:30 +00:00
Misty Release Bot
f4282c091b Merge commit '791551098cb4a56edbae824e45b6f0a10138695b' into v2.x 2023-03-01 15:51:22 +00:00
Barış Soner Uşaklı
791551098c fix: display 25 topics on category feed 2023-02-27 09:47:28 -05:00
Barış Soner Uşaklı
ec58700f6d fix: object destructuring overwriting type parameter
also check for valid types in generateExport
2023-02-27 09:10:53 -05:00
Barış Soner Uşaklı
8cf4a6f62e fix: alert on page load 2023-02-24 13:40:37 -05:00
Eldor
3bd9a87154 fix: show error alert if password change fails 2023-02-22 09:06:51 -05:00
gasoved
edd2fc38fc fix: update main post timestamp when rescheduling 2023-02-16 14:10:07 -05:00
Julian Lam
1b29dbb69d test: add dummy emailer hook in authentication test 2023-02-13 12:15:45 -05:00
Julian Lam
40e7b86da9 docs: update openapi spec to include info about passing in timestamps for topic creation, removing timestamp as valid request param for topic replying 2023-02-13 11:44:54 -05:00
Barış Soner Uşaklı
326b92687f fix: show admins/globalmods if content is purged 2023-02-08 17:35:38 -05:00
Barış Soner Uşaklı
e335d0f601 fix: email expiry timestamps
emailConfirmExpiry is hours and default is 24
2023-02-08 13:22:16 -05:00
Barış Soner Uşaklı
845c8013b6 fix: #11259, clean old emails when updating via admin (#11260)
when admin is changing users emails check if its avaiable and remove old email of user first
upgrade script to cleanup email:uid, email:sorted, will remove entries if user doesn't exist or doesn't have email or if entry in user hash doesn't match entry in email:uid
fix missing ! in email interstitial
fix missing await in canSendValidation,
fix broken tests
dont pass sessionId to email.remove if admin is changing/removing email
2023-02-06 10:45:01 -05:00
Julian Lam
7a5bcc2171 fix: #11257, onSuccessfulLogin called with improper uid 2023-02-03 16:01:31 -05:00
Misty Release Bot
af6ce44737 chore: incrementing version number - v2.8.6
(cherry picked from commit 76732140f3)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2023-02-03 16:39:40 +00:00
Misty Release Bot
f3306d038a chore: update changelog for v2.8.6 2023-02-03 16:39:40 +00:00
Misty Release Bot
76732140f3 chore: incrementing version number - v2.8.6 2023-02-03 16:39:40 +00:00
Misty Release Bot
c6681a1725 Merge commit 'bf92ee0e5fcd0b7a69bb58ec4baaf3b6225ebd6b' into v2.x 2023-02-03 16:39:38 +00:00
Barış Soner Uşaklı
bf92ee0e5f feat: add sitemap filter hooks for categories/topic pages 2023-02-03 09:46:02 -05:00
Misty Release Bot
8335f90ae0 chore(i18n): fallback strings for new resources: nodebb.error 2023-02-02 13:37:19 -05:00
nesro
202378b939 fix: #11254, return check for reroll property 2023-02-02 09:15:04 -05:00
Barış Soner Uşaklı
705cd13ad3 fix: closes #11249, notification uses displayname 2023-01-31 17:27:25 -05:00
Julian Lam
b5598a6e5d fix: wrong link to topics in acp dashboard 2023-01-30 15:05:57 -05:00
Barış Soner Uşaklı
c241baf641 feat: closes #11241, add missing error lang keys 2023-01-30 12:40:24 -05:00
Barış Soner Uşaklı
d68352cce5 lint: remove unused 2023-01-30 12:35:08 -05:00
Barış Soner Uşaklı
0713482bd4 feat: #11240, only show relevant users in flags assignee list
for user flags-> admins + all users who have `admin:users` privilege
for post flags -> admins + global mods + moderators of the category the post is in
refactor getModeratorUids function so it can be used for different privileges
2023-01-30 12:26:08 -05:00
Barış Soner Uşaklı
1d3c0e5a2b fix: https://github.com/NodeBB/NodeBB/issues/11239
parseInt uid received from req.body._uid or req.query._uid
2023-01-30 11:48:10 -05:00
Barış Soner Uşaklı
6d819b056e fix: notif filter selecte field 2023-01-30 10:43:02 -05:00
Misty Release Bot
bff5ce2d79 chore: incrementing version number - v2.8.5
(cherry picked from commit 93ccf604db)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2023-01-27 14:35:25 +00:00
Misty Release Bot
24e58c2895 chore: update changelog for v2.8.5 2023-01-27 14:35:25 +00:00
Misty Release Bot
93ccf604db chore: incrementing version number - v2.8.5 2023-01-27 14:35:24 +00:00
Misty Release Bot
4821b21e81 Merge commit 'f6c96948fe7cee13575ab9c93af6fe7fb9d7b722' into v2.x 2023-01-27 14:35:21 +00:00
Peter Jaszkowiak
f6c96948fe fix: import resolution within plugin modules (#11219)
use module.exports = require('..')
export * from '..' didn't work in some cases
2023-01-27 09:17:38 -05:00
Misty Release Bot
a46b2bbc45 chore: incrementing version number - v2.8.4
(cherry picked from commit b9553613ab)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2023-01-26 14:38:07 +00:00
Misty Release Bot
c13f0e2128 chore: update changelog for v2.8.4 2023-01-26 14:38:07 +00:00
Misty Release Bot
ce924eca0d Merge commit 'c3653bee60740e410bf28808e29ffed6ab373bf9' into v2.x 2023-01-26 14:38:03 +00:00
Misty Release Bot
c20b20a7aa chore: incrementing version number - v2.8.3
(cherry picked from commit 4c46ff42f6)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2023-01-25 19:37:34 +00:00
Misty Release Bot
82eb55d77d Merge commit '89e059a0841f4265d16b28a99ebf847dd10fa055' into v2.x 2023-01-25 19:37:31 +00:00
Misty Release Bot
050e43f8b4 chore: incrementing version number - v2.8.2
(cherry picked from commit 1d5eff2365)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2023-01-13 18:38:13 +00:00
Misty Release Bot
9b6dad367d Merge commit '25ae58e8a057d9c640fbb50f675eadcdbe442aa9' into v2.x 2023-01-13 18:38:09 +00:00
Misty Release Bot
727f879e5b chore: incrementing version number - v2.8.1
(cherry picked from commit 96bdbf52b8)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-12-30 20:48:48 +00:00
Misty Release Bot
fe662f3a46 Merge commit '8a69e740a859cf2eb4a12a0167c1ac76a48c33db' into v2.x 2022-12-30 20:48:22 +00:00
Misty Release Bot
8e77673d39 chore: incrementing version number - v2.8.0
(cherry picked from commit 7ce758d698)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-12-21 22:10:49 +00:00
Misty Release Bot
3f950d5162 Merge commit 'ef500af8e6c618d86069cbf0be0d21e8c3f6e527' into v2.x 2022-12-21 22:10:47 +00:00
Misty Release Bot
96cc0617c5 chore: incrementing version number - v2.7.0
(cherry picked from commit 098097257d)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-12-14 19:36:38 +00:00
Misty Release Bot
ccf8739344 Merge commit '9ee8502d7a8ba41ce6ded74b1ce1fbbe180b1dda' into v2.x 2022-12-14 19:36:36 +00:00
Misty Release Bot
7e52a7a574 chore: incrementing version number - v2.6.1
(cherry picked from commit f8e947e2a7)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-11-28 01:01:10 +00:00
Misty Release Bot
21d9806ca9 Merge commit '48d143921753914da45926cca6370a92ed0c46b8' into v2.x 2022-11-28 01:00:52 +00:00
Misty Release Bot
e7fcf482f3 chore: incrementing version number - v2.6.0
(cherry picked from commit 12f0541dfa)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-11-23 19:04:45 +00:00
Misty Release Bot
d80c80b618 Merge commit 'c7aa4ebf47f7b87db1f5efa0c9662b21cff7b194' into v2.x 2022-11-23 19:04:37 +00:00
Misty Release Bot
dec0e7deac chore: incrementing version number - v2.5.8
(cherry picked from commit 466263172a)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-11-09 18:46:09 +00:00
Misty Release Bot
c7ff98a12d Merge commit '2f9d8c350e54543f608d3d4c8e1a49bbb6cdea38' into v2.x 2022-11-09 18:42:47 +00:00
Misty Release Bot
5836bf4a05 chore: incrementing version number - v2.5.7
(cherry picked from commit dd6d104820)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-10-14 15:59:58 +00:00
Misty Release Bot
a5357812c6 Merge commit 'dc4a850cacecb8c57923803363dac9bb61221bba' into v2.x 2022-10-14 15:59:56 +00:00
Misty Release Bot
c7bd7dbfe6 chore: incrementing version number - v2.5.6
(cherry picked from commit 7dc45afa4c)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-10-13 14:21:02 +00:00
Misty Release Bot
ec4dadabd4 Merge commit '67efaeb4b8e03417dfc3b575f19249f18f4cb3d6' into v2.x 2022-10-13 14:21:00 +00:00
Misty Release Bot
3509ed9461 chore: incrementing version number - v2.5.5
(cherry picked from commit 58b2f10ee9)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-10-11 17:07:16 +00:00
Misty Release Bot
cb8d94563a Merge commit 'b91ef6dd761d643383d1eb4f4ac3abd5e55c18e5' into v2.x 2022-10-11 17:07:09 +00:00
Misty Release Bot
e83260ca28 chore: incrementing version number - v2.5.4
(cherry picked from commit 89eb0340d1)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-10-11 12:25:36 +00:00
Misty Release Bot
4bf1ce42e6 Merge commit 'ebd5dcc6d62841dbcd120351919cdf7cf59f5933' into v2.x 2022-10-11 12:25:01 +00:00
Misty Release Bot
7e922936d0 chore: incrementing version number - v2.5.3
(cherry picked from commit cf6e8101e8)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-09-19 16:23:59 +00:00
Misty Release Bot
3c8ce70c74 Merge commit 'cf4f5447bb168b9bac32ac7ddbe567f273966b88' into v2.x 2022-09-19 16:23:38 +00:00
Misty Release Bot
babcd17e6c chore: incrementing version number - v2.5.2
(cherry picked from commit e351fbe89c)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-09-04 14:57:03 +00:00
Misty Release Bot
ec6ffaad4e Merge commit 'b45e24139092af6c3d50851a31452b9d28953fdd' into v2.x 2022-09-04 14:54:41 +00:00
Misty Release Bot
ce3aa95053 chore: incrementing version number - v2.5.1
(cherry picked from commit 2bf475299d)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-09-02 19:14:02 +00:00
Misty Release Bot
7aab01d87a Merge commit '67cb70352f994d8fab3477f0d753e0dd588bab70' into v2.x 2022-09-02 19:14:00 +00:00
Misty Release Bot
01d276cbee chore: incrementing version number - v2.5.0
(cherry picked from commit c3e19005f6)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-09-01 16:14:07 +00:00
Misty Release Bot
9758b7af2c Merge commit '8fe41d92a261ee00820a2b270f67d8baf8d84461' into v2.x 2022-09-01 15:23:08 +00:00
Misty Release Bot
dd3e1a2861 chore: incrementing version number - v2.4.5
(cherry picked from commit d8b1291088)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-08-22 16:14:00 +00:00
Misty Release Bot
2a97342035 Merge commit '9b96c33d5d3706f9c5795b9c07ace063f69b101d' into v2.x 2022-08-22 16:13:55 +00:00
Misty Release Bot
d5525c873b chore: incrementing version number - v2.4.4
(cherry picked from commit 24221d66e0)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-08-18 13:45:27 +00:00
Misty Release Bot
e7c3634f9a Merge commit 'fc9b436f3ef9d0ef335967456b6f6890ee8560b1' into v2.x 2022-08-18 13:45:18 +00:00
Misty Release Bot
9c647c6ce2 chore: incrementing version number - v2.4.3
(cherry picked from commit be0256b26e)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-08-18 02:33:19 +00:00
Misty Release Bot
52fc05edfe Merge commit '4dc7fa050f1f30888b5bd71622b68537cc032b44' into v2.x 2022-08-18 02:33:06 +00:00
Misty Release Bot
3aa7b8552a chore: incrementing version number - v2.4.2
(cherry picked from commit 1635633acd)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-08-17 21:12:35 +00:00
Misty Release Bot
36523c67b8 Merge commit 'ec048a01ba9f2dbc17064427bdcafd88e7271c88' into v2.x 2022-08-17 21:12:23 +00:00
Misty Release Bot
60cbd1480d chore: incrementing version number - v2.4.1
(cherry picked from commit 7f5ff2e613)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-08-14 00:18:25 +00:00
Misty Release Bot
f3e59508ae Merge commit '15ca460c8f144c3167249b135902ac59289ca2f8' into v2.x 2022-08-14 00:18:05 +00:00
Misty Release Bot
4834cde335 chore: incrementing version number - v2.4.0
(cherry picked from commit 5525442279)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-08-10 20:02:19 +00:00
Misty Release Bot
01da76e1dc Merge commit '9b753d6d57b850ef5ebc50e5a3dd7b2cbe4d5a27' into v2.x 2022-08-10 20:02:08 +00:00
Misty Release Bot
d2425942a6 chore: incrementing version number - v2.3.1
(cherry picked from commit 44dd42dc89)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-07-29 15:26:23 +00:00
Misty Release Bot
8d7475be7b Merge commit '89173f17cab6f6447647e5a3d8609f97c09084d1' into v2.x 2022-07-29 15:26:17 +00:00
Misty Release Bot
046ea12022 chore: incrementing version number - v2.3.0
(cherry picked from commit e616b2e16d)
Signed-off-by: Misty Release Bot <deploy@nodebb.org>
2022-07-28 18:21:07 +00:00
107 changed files with 859 additions and 248 deletions

View File

@@ -13,13 +13,14 @@ on:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
permissions:
contents: read
packages: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -32,14 +33,15 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: nodebb/docker
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}

View File

@@ -1,3 +1,155 @@
#### v2.8.11 (2023-04-11)
##### Chores
* incrementing version number - v2.8.10 (5b7c3671)
* update changelog for v2.8.10 (188ec62f)
##### Continuous Integration
* publish to ghcr instead of docker hub (c2756728)
##### Documentation Changes
* update readme with new screenshot and updated copy for Harmony (67055006)
##### Bug Fixes
* don't crash on objects with toString property (4d2d7689)
* fire action:user.online on user login (7397873d)
##### Tests
* update socket.io test (e0b20658)
#### v2.8.10 (2023-03-27)
##### Chores
* up composer-default (e9a8e195)
* incrementing version number - v2.8.9 (57f14e41)
* update changelog for v2.8.9 (18b2150e)
##### Bug Fixes
* #11403, remove loader.js crash counter logic (830f142b)
* don't crash if event name is not a string (37b48b82)
* closes #11173, move cache clear code (c2961ad4)
##### Other Changes
* fix arrow (1aff9cad)
* whitespace (894f392b)
#### v2.8.9 (2023-03-19)
##### Chores
* up cron (73a50d17)
* incrementing version number - v2.8.8 (b331b942)
* update changelog for v2.8.8 (c03d5db7)
##### Bug Fixes
* thumb remove on windows, closes #11357 (767c1d1f)
* #11357 clear cache on thumb remove (a3a38e4b)
* closes #11352, try/catch rss feeds (cfd50272)
* closes #11343, don't crash if tags array is empty (56427e4f)
##### Code Style Changes
* more fixes (93aa43f7)
##### Tests
* openapi for thumbs (9e685e65)
#### v2.8.8 (2023-03-09)
##### Chores
* incrementing version number - v2.8.7 (3f8248d6)
* update changelog for v2.8.7 (2ca38e7b)
##### Bug Fixes
* stop topic navigation hotkeys from firing if in a mousetrap-enabled form element (22fc8fe3)
* stop topic navigation hotkeys from firing if in a mousetrap-enabled form element (17d0b40e)
* tag filtering when changing filter to watched topics (1545223e)
* get cid from pid instead of passing in (f054a4f4)
* closes #11331, allow 0 length content if set to 0 in acp (8c762d32)
#### v2.8.7 (2023-03-01)
##### Chores
* incrementing version number - v2.8.6 (af6ce447)
* update changelog for v2.8.6 (f3306d03)
##### Documentation Changes
* update openapi spec to include info about passing in timestamps for topic creation, removing timestamp as valid request param for topic replying (40e7b86d)
##### Bug Fixes
* display 25 topics on category feed (79155109)
* object destructuring overwriting type parameter (ec58700f)
* alert on page load (8cf4a6f6)
* show error alert if password change fails (3bd9a871)
* update main post timestamp when rescheduling (edd2fc38)
* show admins/globalmods if content is purged (326b9268)
* email expiry timestamps (e335d0f6)
* #11259, clean old emails when updating via admin (#11260) (845c8013)
* #11257, onSuccessfulLogin called with improper uid (7a5bcc21)
##### Tests
* add dummy emailer hook in authentication test (1b29dbb6)
#### v2.8.6 (2023-02-03)
##### Chores
* **i18n:** fallback strings for new resources: nodebb.error (8335f90a)
* incrementing version number - v2.8.5 (bff5ce2d)
* update changelog for v2.8.5 (24e58c28)
##### New Features
* add sitemap filter hooks for categories/topic pages (bf92ee0e)
* closes #11241, add missing error lang keys (c241baf6)
* #11240, only show relevant users in flags assignee list (0713482b)
##### Bug Fixes
* #11254, return check for reroll property (202378b9)
* closes #11249, notification uses displayname (705cd13a)
* wrong link to topics in acp dashboard (b5598a6e)
* https://github.com/NodeBB/NodeBB/issues/11239 (1d3c0e5a)
* notif filter selecte field (6d819b05)
##### Other Changes
* remove unused (d68352cc)
#### v2.8.5 (2023-01-27)
##### Chores
* incrementing version number - v2.8.4 (a46b2bbc)
* update changelog for v2.8.4 (c13f0e21)
##### Bug Fixes
* import resolution within plugin modules (#11219) (f6c96948)
#### v2.8.4 (2023-01-26)
##### Chores
* incrementing version number - v2.8.3 (c20b20a7)
* update changelog for v2.8.3 (eb2841ee)
#### v2.8.3 (2023-01-25)
##### Chores
@@ -5,28 +157,6 @@
* remove extraneous lines from changelog (48c9f447)
* incrementing version number - v2.8.2 (050e43f8)
* update changelog for v2.8.2 (66aa3169)
* incrementing version number - v2.8.1 (727f879e)
* incrementing version number - v2.8.0 (8e77673d)
* incrementing version number - v2.7.0 (96cc0617)
* incrementing version number - v2.6.1 (7e52a7a5)
* incrementing version number - v2.6.0 (e7fcf482)
* incrementing version number - v2.5.8 (dec0e7de)
* incrementing version number - v2.5.7 (5836bf4a)
* incrementing version number - v2.5.6 (c7bd7dbf)
* incrementing version number - v2.5.5 (3509ed94)
* incrementing version number - v2.5.4 (e83260ca)
* incrementing version number - v2.5.3 (7e922936)
* incrementing version number - v2.5.2 (babcd17e)
* incrementing version number - v2.5.1 (ce3aa950)
* incrementing version number - v2.5.0 (01d276cb)
* incrementing version number - v2.4.5 (dd3e1a28)
* incrementing version number - v2.4.4 (d5525c87)
* incrementing version number - v2.4.3 (9c647c6c)
* incrementing version number - v2.4.2 (3aa7b855)
* incrementing version number - v2.4.1 (60cbd148)
* incrementing version number - v2.4.0 (4834cde3)
* incrementing version number - v2.3.1 (d2425942)
* incrementing version number - v2.3.0 (046ea120)
##### Bug Fixes

View File

@@ -2,7 +2,7 @@
"name": "nodebb",
"license": "GPL-3.0",
"description": "NodeBB Forum",
"version": "2.8.4",
"version": "2.8.12",
"homepage": "http://www.nodebb.org",
"repository": {
"type": "git",
@@ -53,9 +53,9 @@
"connect-pg-simple": "8.0.0",
"connect-redis": "6.1.3",
"cookie-parser": "1.4.6",
"cron": "2.1.0",
"cron": "2.3.0",
"cropperjs": "1.5.13",
"csurf": "1.11.0",
"csrf-sync": "4.0.0",
"daemon": "1.1.0",
"diff": "5.1.0",
"esbuild": "0.16.10",
@@ -90,7 +90,7 @@
"@nodebb/bootswatch": "3.4.2",
"nconf": "0.12.0",
"nodebb-plugin-2factor": "5.1.2",
"nodebb-plugin-composer-default": "9.2.4",
"nodebb-plugin-composer-default": "9.2.5",
"nodebb-plugin-dbsearch": "5.1.5",
"nodebb-plugin-emoji": "4.0.6",
"nodebb-plugin-emoji-android": "3.0.0",

View File

@@ -30,9 +30,7 @@ const output = logrotate({ file: outputLogFilePath, size: '1m', keep: 3, compres
const silent = nconf.get('silent') === 'false' ? false : nconf.get('silent') !== false;
let numProcs;
const workers = [];
const Loader = {
timesStarted: 0,
};
const Loader = {};
const appPath = path.join(__dirname, 'app.js');
Loader.init = function () {
@@ -57,21 +55,6 @@ Loader.displayStartupMessages = function () {
Loader.addWorkerEvents = function (worker) {
worker.on('exit', (code, signal) => {
if (code !== 0) {
if (Loader.timesStarted < numProcs * 3) {
Loader.timesStarted += 1;
if (Loader.crashTimer) {
clearTimeout(Loader.crashTimer);
}
Loader.crashTimer = setTimeout(() => {
Loader.timesStarted = 0;
}, 10000);
} else {
console.log(`${numProcs * 3} restarts in 10 seconds, most likely an error on startup. Halting.`);
process.exit();
}
}
console.log(`[cluster] Child Process (${worker.pid}) has exited (code: ${code}, signal: ${signal})`);
if (!(worker.suicide || code === 0)) {
console.log('[cluster] Spinning up another process...');

View File

@@ -62,6 +62,7 @@
"no-user": "اسم مستخدم غير موجود",
"no-teaser": "مقتطف غير موجود",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "لاتملك الصلاحيات اللازمة للقيام بهذه العملية",
"category-disabled": "قائمة معطلة",
"topic-locked": "الموضوع مقفول",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "لقد شاركت بالتصويت ، ألا تذكر؟",
"reputation-system-disabled": "نظام السمعة معطل",
"downvoting-disabled": "التصويتات السلبية معطلة",

View File

@@ -62,6 +62,7 @@
"no-user": "Потребителят не съществува",
"no-teaser": "Резюмето не съществува",
"no-flag": "Докладът не съществува",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Нямате достатъчно права за това действие.",
"category-disabled": "Категорията е изключена",
"topic-locked": "Темата е заключена",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Това съобщение вече е изтрито.",
"chat-restored-already": "Това съобщение вече е възстановено.",
"chat-room-does-not-exist": "Стаята за разговори не съществува.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Вече сте дали глас за тази публикация.",
"reputation-system-disabled": "Системата за репутация е изключена.",
"downvoting-disabled": "Отрицателното гласуване е изключено",

View File

@@ -62,6 +62,7 @@
"no-user": "এই নামে কোন সদস্য নেই",
"no-teaser": "টিজারটি খুজে পাওয়া যায় নি",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "এই কাজটির জন্য আপনার পর্যাপ্ত অধিকার নেই",
"category-disabled": "বিভাগটি নিষ্ক্রিয়",
"topic-locked": "টপিক বন্ধ",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "সম্মাননা ব্যাবস্থা নিস্ক্রীয় রাখা হয়েছে",
"downvoting-disabled": "ঋণাত্মক ভোট নিস্ক্রীয় রাখা হয়েছে।",

View File

@@ -62,6 +62,7 @@
"no-user": "Uživatel neexistuje",
"no-teaser": "Chyták neexistuje",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Na tuto akci nemáte dostatečné oprávnění.",
"category-disabled": "Kategorie zakázána",
"topic-locked": "Téma uzamknuto",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Tato konverzační zpráva již byla odstraněna.",
"chat-restored-already": "Tato konverzační zpráva již byla obnovena.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Již jste v tomto příspěvku hlasoval.",
"reputation-system-disabled": "Systém reputací je zakázán.",
"downvoting-disabled": "Systém nesouhlasu je zakázán",

View File

@@ -62,6 +62,7 @@
"no-user": "Brugeren eksisterer ikke",
"no-teaser": "Teaser eksisterer ikke",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Du har ikke nok rettigheder til at udføre denne handling",
"category-disabled": "Kategorien er deaktiveret",
"topic-locked": "Tråden er låst",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Vurderingssystem er slået fra.",
"downvoting-disabled": "Nedvurdering er slået fra",

View File

@@ -62,6 +62,7 @@
"no-user": "Der Benutzer existiert nicht",
"no-teaser": "Zusammenfassung existiert nicht",
"no-flag": "Markierung existiert nicht",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Du verfügst nicht über ausreichende Berechtigungen, um die Aktion durchzuführen.",
"category-disabled": "Kategorie ist deaktiviert",
"topic-locked": "Thema ist gesperrt",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Diese Chatnachricht wurde bereits gelöscht.",
"chat-restored-already": "Diese Chatnachricht wurde bereits wiederhergestellt.",
"chat-room-does-not-exist": "Der Chatraum existiert nicht.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Du hast diesen Beitrag bereits bewertet.",
"reputation-system-disabled": "Das Reputationssystem ist deaktiviert.",
"downvoting-disabled": "Downvotes sind deaktiviert.",

View File

@@ -62,6 +62,7 @@
"no-user": "User does not exist",
"no-teaser": "Teaser does not exist",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "You do not have enough privileges for this action.",
"category-disabled": "Η κατηγορία έχει απενεργοποιηθεί",
"topic-locked": "Το θέμα έχει κλειδωθεί",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Το σύστημα φήμης έχει απενεργοποιηθεί.",
"downvoting-disabled": "Η καταψήφιση έχει απενεργοποιηθεί",

View File

@@ -70,6 +70,7 @@
"no-user": "User does not exist",
"no-teaser": "Teaser does not exist",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "You do not have enough privileges for this action.",
"category-disabled": "Category disabled",
@@ -182,6 +183,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Reputation system is disabled.",

View File

@@ -62,6 +62,7 @@
"no-user": "User does not exist",
"no-teaser": "Teaser does not exist",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "You do not have enough privileges for this action.",
"category-disabled": "Category disabled",
"topic-locked": "Topic Locked",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Reputation system is disabled.",
"downvoting-disabled": "Downvoting is disabled",

View File

@@ -62,6 +62,7 @@
"no-user": "User does not exist",
"no-teaser": "Teaser does not exist",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "You do not have enough privileges for this action.",
"category-disabled": "Category disabled",
"topic-locked": "Topic Locked",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Reputation system is disabled.",
"downvoting-disabled": "Downvoting is disabled",

View File

@@ -62,6 +62,7 @@
"no-user": "El usuario no existe",
"no-teaser": "El resumen no existe",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "No tienes suficientes privilegios para realizar esta acción.",
"category-disabled": "Categoría deshabilitada",
"topic-locked": "Tema bloqueado",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Este mensaje de chat ya ha sido borrado.",
"chat-restored-already": "Este mensaje de chat ya ha sido restaurado.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Ya has votado a este mensaje.",
"reputation-system-disabled": "El sistema de reputación está deshabilitado.",
"downvoting-disabled": "La votación negativa está deshabilitada.",

View File

@@ -62,6 +62,7 @@
"no-user": "Kasutajat ei eksisteeri",
"no-teaser": "Eelvaadet ei eksisteeri",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Sul pole piisavalt õigusi.",
"category-disabled": "Kategooria keelatud",
"topic-locked": "Teema lukustatud",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Sa oled juba hääletanud sellel postitusel.",
"reputation-system-disabled": "Reputatsiooni süsteem ei ole aktiveeritud",
"downvoting-disabled": "Negatiivsete häälte andmine ei ole võimaldatud",

View File

@@ -62,6 +62,7 @@
"no-user": "کاربر وجود ندارد",
"no-teaser": "تیزر وجود ندارد",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "شما دسترسی کافی برای این کار را ندارید",
"category-disabled": "دسته غیر‌فعال شد.",
"topic-locked": "موضوع بسته شد.",
@@ -156,6 +157,9 @@
"chat-deleted-already": "این پیام قبلا حذف شده است",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "شما قبلا به این پست رای داده اید.",
"reputation-system-disabled": "سیستم اعتبار غیر فعال شده است",
"downvoting-disabled": "رأی منفی غیر فعال شده است",

View File

@@ -62,6 +62,7 @@
"no-user": "Käyttäjää ei ole olemassa",
"no-teaser": "Teaser does not exist",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Oikeutesi eivät riitä toiminnon suorittamiseen.",
"category-disabled": "Kategoria ei ole käytössä",
"topic-locked": "Aihe lukittu",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Reputation system is disabled.",
"downvoting-disabled": "Downvoting is disabled",

View File

@@ -62,6 +62,7 @@
"no-user": "Cet utilisateur n'existe pas",
"no-teaser": "Laperçu n'existe pas",
"no-flag": "Le signalement n'existe pas",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Vous n'avez pas les privilèges nécessaires pour effectuer cette action.",
"category-disabled": "Catégorie désactivée",
"topic-locked": "Sujet verrouillé",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Ce message a déjà été supprimé.",
"chat-restored-already": "Ce message de discussion a déjà été restauré.",
"chat-room-does-not-exist": "Le salon de discussion n'existe pas.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Vous avez déjà voté pour ce message.",
"reputation-system-disabled": "Le système de réputation est désactivé",
"downvoting-disabled": "Les votes négatifs ne sont pas autorisés",

View File

@@ -62,6 +62,7 @@
"no-user": "O usuario non existe",
"no-teaser": "A vista previa do tema non existe",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Non tes privilexios dabondo para ver este tema.",
"category-disabled": "Categoría deshabilitada",
"topic-locked": "Tema Pechado",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Xa votache esta mensaxe.",
"reputation-system-disabled": "O sistema de reputación está deshabilitado",
"downvoting-disabled": "Os votos negativos están deshabilitados",

View File

@@ -62,6 +62,7 @@
"no-user": "משתמש אינו קיים",
"no-teaser": "תקציר אינו קיים",
"no-flag": "דיווח לא קיים",
"no-chat-room": "Chat room does not exist",
"no-privileges": "ההרשאות שלכם אינן מספיקות לביצוע פעולה זו.",
"category-disabled": "קטגוריה לא פעילה",
"topic-locked": "נושא נעול",
@@ -156,6 +157,9 @@
"chat-deleted-already": "הודעת צ'אט זו כבר נמחקה.",
"chat-restored-already": "הודעת צ'אט זו כבר שוחזרה.",
"chat-room-does-not-exist": "חדר צ'אט אינו קיים.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "הצבעתם כבר בנושא זה.",
"reputation-system-disabled": "מערכת המוניטין לא פעילה.",
"downvoting-disabled": "היכולת להצביע נגד מושבתת",

View File

@@ -62,6 +62,7 @@
"no-user": "Korisnik ne postoji",
"no-teaser": "Zadirkivač ne postoji",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Nemate privilegije za ovu radnju.",
"category-disabled": "Kategorija onemogućena",
"topic-locked": "Tema zaključana",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Već ste glasali za ovu objavu",
"reputation-system-disabled": "Sistem reputacije onemogućen.",
"downvoting-disabled": "Oduzimanje glasova je onemogućeno",

View File

@@ -62,6 +62,7 @@
"no-user": "Nem létező felhasználó",
"no-teaser": "A bevezető nem létezik",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Nincs elég jogod ehhez a művelethez.",
"category-disabled": "Kategória kikapcsolva",
"topic-locked": "Téma lezárva",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Ez az üzenet már törölve lett.",
"chat-restored-already": "Ez az üzenet már vissza van állítva.",
"chat-room-does-not-exist": "Csevegő szoba nem létezik.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Már szavaztál erre a hozzászólásra.",
"reputation-system-disabled": "Hírnév funkció kikapcsolva.",
"downvoting-disabled": "Leszavazás funkció kikapcsolva",

View File

@@ -62,6 +62,7 @@
"no-user": "Օգտվողը գոյություն չունի",
"no-teaser": "Թիզերը գոյություն չունի",
"no-flag": "Դրոշ գոյություն չունի",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Դուք չունեք բավարար արտոնություններ այս գործողության համար:",
"category-disabled": "Կատեգորիան անջատված է",
"topic-locked": "Թեման փակված է",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Այս զրույցի հաղորդագրությունն արդեն ջնջված է",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Այս զրուցարանը գոյություն չունի:",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Դուք արդեն քվեարկել եք այս գրառման օգտին:",
"reputation-system-disabled": "Վարկանիշի համակարգը անջատված է:",
"downvoting-disabled": "Դեմ քվեարկությունն անջատված է",

View File

@@ -62,6 +62,7 @@
"no-user": "Pengguna tidak ditemukan",
"no-teaser": "Teaser tidak ditemukan",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Kamu tidak punya cukup izin untuk melakukan ini",
"category-disabled": "Kategori ditiadakan",
"topic-locked": "Topik dikunci",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Sistem reputasi ditiadakan.",
"downvoting-disabled": "Downvoting ditiadakan",

View File

@@ -62,6 +62,7 @@
"no-user": "L'Utente non esiste",
"no-teaser": "Teaser non esiste",
"no-flag": "Segnalazione non esiste",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Non hai abbastanza privilegi per questa azione.",
"category-disabled": "Categoria disabilitata",
"topic-locked": "Discussione Bloccata",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Il messaggio è già stato eliminato.",
"chat-restored-already": "Questo messaggio della chat è già stato ripristinato.",
"chat-room-does-not-exist": "La stanza chat non esiste.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Hai già votato per questo post",
"reputation-system-disabled": "Il sistema di reputazione è disabilitato.",
"downvoting-disabled": "Votata negativamente è disabilitato",

View File

@@ -62,6 +62,7 @@
"no-user": "ユーザーは存在しません",
"no-teaser": "ティーザーが存在しません",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "あなたがこの行為する権利がありません。",
"category-disabled": "この板は無効された",
"topic-locked": "スレッドがロックされた",
@@ -156,6 +157,9 @@
"chat-deleted-already": "このチャットメッセージは既に削除されています",
"chat-restored-already": "このチャットメッセージは既に削除されています",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "あなたはすでにこの投稿を評価しました。",
"reputation-system-disabled": "Reputation system is disabled.",
"downvoting-disabled": "Downvoting is disabled",

View File

@@ -62,6 +62,7 @@
"no-user": "존재하지 않는 사용자입니다.",
"no-teaser": "존재하지 않는 미리보기입니다.",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "이 작업을 할 수 있는 권한이 없습니다.",
"category-disabled": "카테고리가 비활성화 되었습니다.",
"topic-locked": "게시물이 잠금 상태입니다.",
@@ -156,6 +157,9 @@
"chat-deleted-already": "이미 삭제된 채팅 메시지입니다.",
"chat-restored-already": "이 채팅 메시지는 이미 복원되었습니다.",
"chat-room-does-not-exist": "채팅이 존재하지 않습니다.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "이미 이 포스트에 투표하셨습니다.",
"reputation-system-disabled": "인지도 시스템이 비활성화되어있습니다.",
"downvoting-disabled": "비추천 기능이 비활성 상태입니다.",

View File

@@ -62,6 +62,7 @@
"no-user": "Tokio vartotojo nėra",
"no-teaser": "Anonsas neegzistuoja",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Šiam veiksmui jūs neturite pakankamų privilegijų.",
"category-disabled": "Kategorija išjungta",
"topic-locked": "Tema užrakinta",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Ši žinutė buvo pašalinta",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Jūs jau balsavote už šį pranešimą.",
"reputation-system-disabled": "Reputacijos sistema išjungta.",
"downvoting-disabled": "Downvoting yra išjungtas",

View File

@@ -62,6 +62,7 @@
"no-user": "Lietotājs nav atrasts",
"no-teaser": "Ievadapraksts nav atrasts",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Tev nepietiek tiesības šai darbībai.",
"category-disabled": "Kategorija ir atspējota",
"topic-locked": "Temats ir slēgts",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Saruna jau ir izdzēsta.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Tu jau balsoji par šo rakstu.",
"reputation-system-disabled": "Ranga punktu sistēma ir atspējota.",
"downvoting-disabled": "Balsošana \"pret\" ir atspējota",

View File

@@ -62,6 +62,7 @@
"no-user": "Pengguna tidak wujud",
"no-teaser": "Pengusik tidak wujud",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Anda tidak mempunyai cukup keistimewaan untuk perbuatan ini.",
"category-disabled": "Kategori dilumpuhkan",
"topic-locked": "Topik Dikunci",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Sistem reputasi dilumpuhkan.",
"downvoting-disabled": "Undi turun dilumpuhkan",

View File

@@ -62,6 +62,7 @@
"no-user": "Bruker eksisterer ikke",
"no-teaser": "Teaseren eksisterer ikke",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Du har ikke nok rettigheter til å utføre denne handlingen.",
"category-disabled": "Kategori deaktivert",
"topic-locked": "Emne låst",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Denne meldingen har allerede blitt slettet.",
"chat-restored-already": "Denne meldingen har allerede blitt gjenopprettet.",
"chat-room-does-not-exist": "Dette chatterommet finnes ikke.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Du har allerede stemt på dette innlegget",
"reputation-system-disabled": "Omdømmesystemet er deaktivert.",
"downvoting-disabled": "Nedstemming er deaktivert",

View File

@@ -62,6 +62,7 @@
"no-user": "Gebruiker bestaat niet",
"no-teaser": "Dit voorproefje bestaat niet",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Onvoldoende rechten om deze actie uit te voeren",
"category-disabled": "Categorie uitgeschakeld",
"topic-locked": "Onderwerp gesloten",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Dit chat bericht is al verwijderd.",
"chat-restored-already": "Dit chat bericht is al hersteld.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Je hebt al gestemd voor deze post.",
"reputation-system-disabled": "Reputatie systeem is uitgeschakeld.",
"downvoting-disabled": "Negatief stemmen is uitgeschakeld",

View File

@@ -62,6 +62,7 @@
"no-user": "Użytkownik nie istnieje",
"no-teaser": "Zwiastun nie istnieje",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Nie masz przywileju wykonywania tej akcji",
"category-disabled": "Kategoria wyłączona.",
"topic-locked": "Temat zablokowany",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Ten komunikat czatu jest już skasowany",
"chat-restored-already": "Ta wiadomość została już przywrócona",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Już zagłosowałeś na ten post",
"reputation-system-disabled": "System reputacji jest wyłączony.",
"downvoting-disabled": "Negatywna ocena postów jest wyłączona",

View File

@@ -62,6 +62,7 @@
"no-user": "O usuário não existe",
"no-teaser": "O teaser não existe",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Você não possui privilégios suficientes para esta ação.",
"category-disabled": "Categoria desativada",
"topic-locked": "Tópico Trancado",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Essa mensagem de chat já foi deletada",
"chat-restored-already": "Essa mensagem de chat já foi restaurada.",
"chat-room-does-not-exist": "A sala de chat não existe.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Você já votou neste post.",
"reputation-system-disabled": "O sistema de reputação está desabilitado.",
"downvoting-disabled": "Negativação está desabilitada",

View File

@@ -62,6 +62,7 @@
"no-user": "Utilizador não existente",
"no-teaser": "Não existe pré-visualização",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Não possuis privilégios suficientes para esta ação.",
"category-disabled": "Categoria desativada",
"topic-locked": "Tópico bloqueado",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Esta mensagem já foi apagada.",
"chat-restored-already": "Esta mensagem já foi restaurada.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Já votaste nesta publicação.",
"reputation-system-disabled": "O sistema de reputação está desativado.",
"downvoting-disabled": "Os votos negativos estão desativados",

View File

@@ -62,6 +62,7 @@
"no-user": "Utilizatorul nu exista.",
"no-teaser": "Teaser does not exist",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "You do not have enough privileges for this action.",
"category-disabled": "Categorie dezactivată",
"topic-locked": "Subiect Închis",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Sistemul de reputație este dezactivat.",
"downvoting-disabled": "Votarea negativă este dezactivată",

View File

@@ -62,6 +62,7 @@
"no-user": "Такого пользователя не существует",
"no-teaser": "Такого тизера не существует",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "У вас недостаточно прав для этого действия.",
"category-disabled": "Категория отключена",
"topic-locked": "Тема закрыта",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Это сообщение чата уже удалено.",
"chat-restored-already": "Это сообщение чата уже было восстановлено.",
"chat-room-does-not-exist": "Комната чата не существует.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Вы уже проголосовали за это сообщение.",
"reputation-system-disabled": "Система репутации отключена.",
"downvoting-disabled": "Понижение рейтинга отключено",

View File

@@ -62,6 +62,7 @@
"no-user": "Umuntu utabaho",
"no-teaser": "Inshamake itabaho",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Ntabwo uragira uburenganzira buhagije ngo wemererwe iki gikorwa",
"category-disabled": "Icyiciro cyabujijwe",
"topic-locked": "Ikiganiro Cyafungiranywe",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Ibijyanye n'itangwa ry'amanota ntibyemerewe. ",
"downvoting-disabled": "Kwambura amanota ntibyemerewe",

View File

@@ -62,6 +62,7 @@
"no-user": "User does not exist",
"no-teaser": "Teaser does not exist",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "You do not have enough privileges for this action.",
"category-disabled": "Category disabled",
"topic-locked": "Topic Locked",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Reputation system is disabled.",
"downvoting-disabled": "Downvoting is disabled",

View File

@@ -62,6 +62,7 @@
"no-user": "Užívateľ neexistuje",
"no-teaser": "Ukážka neexistuje",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Na túto akciu nemáte dostatočné oprávnenia.",
"category-disabled": "Kategória je zablokovaná",
"topic-locked": "Téma je uzamknutá",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Táto správa konverzácie už bola odstránená.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Za tento príspevok ste už hlasovali.",
"reputation-system-disabled": "Systém reputácie je zablokovaný.",
"downvoting-disabled": "Hlasovanie proti je zablokované",

View File

@@ -62,6 +62,7 @@
"no-user": "Uporabnik ne obstaja.",
"no-teaser": "Predogled ne obstaja.",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Nimate dovolj pravic za to dejanje.",
"category-disabled": "Kategorija je onemogočena.",
"topic-locked": "Tema je zaklenjena.",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Za to objavo ste že glasovali.",
"reputation-system-disabled": "Sistem za ugled je onemogočen.",
"downvoting-disabled": "Negativno glasovanje je onemogočeno.",

View File

@@ -62,6 +62,7 @@
"no-user": "Përdoruesi nuk ekziston",
"no-teaser": "Përmbledhja nuk ekziston",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Nuk keni akses të mjaftueshem për këtë veprim.",
"category-disabled": "Kategori e çaktivizuar",
"topic-locked": "Temë e kyçur",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Ky mesazh është fshirë tashmë.",
"chat-restored-already": "Ky mesazh është rikthyer tashmë.",
"chat-room-does-not-exist": "Kjo dhomë bisede nuk ekziston.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Ju keni votuar tashmë për këtë postim.",
"reputation-system-disabled": "Sistemi i reputacionit është i çaktivizuar.",
"downvoting-disabled": "Votimi kundër është i çaktivizuar",

View File

@@ -62,6 +62,7 @@
"no-user": "Корисник не постоји",
"no-teaser": "Исечак не постоји",
"no-flag": "Заставица не постоји",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Немате довољне привилегије за обављање ове радње.",
"category-disabled": "Категорија је онемогућена",
"topic-locked": "Тема је закључана",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Ова порука ћаскања је већ избрисана.",
"chat-restored-already": "Ова порука ћаскања је већ обновљена.",
"chat-room-does-not-exist": "Соба за ћаскање не постоји.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Већ сте гласали за ову поруку.",
"reputation-system-disabled": "Угледи су онемогућени.",
"downvoting-disabled": "Негативно гласање је онемогућено",

View File

@@ -62,6 +62,7 @@
"no-user": "Användaren finns inte",
"no-teaser": "Förhandsvisningen finns inte",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Du har inte tillräckliga rättigheter för den här åtgärden.",
"category-disabled": "Kategorin inaktiverad",
"topic-locked": "Ämnet låst",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Detta chattmeddelande har redan raderats.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Du har redan röstat på det här inlägget.",
"reputation-system-disabled": "Ryktessystemet är inaktiverat.",
"downvoting-disabled": "Nedröstning är inaktiverat",

View File

@@ -62,6 +62,7 @@
"no-user": "ยังไม่มีผู้ใช้งานนี้",
"no-teaser": "ยังไม่มีทีเซอร์นี้",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "คุณมีสิทธิ์ไม่เพียงพอที่จะทำรายการนี้",
"category-disabled": "Category นี้ถูกปิดการใช้งานแล้ว",
"topic-locked": "กระทู้ถูกล็อก",
@@ -156,6 +157,9 @@
"chat-deleted-already": "This chat message has already been deleted.",
"chat-restored-already": "This chat message has already been restored.",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "คุณได้โหวตโพสต์นี้แล้ว",
"reputation-system-disabled": "ระบบชื่อเสียงถูกปิดใช้งาน",
"downvoting-disabled": "\"การโหวตลง\" ถูกปิดใช้งาน",

View File

@@ -62,6 +62,7 @@
"no-user": "Kullanıcı Yok",
"no-teaser": "İleti Yok",
"no-flag": "Şikayet Yok",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Bu işlemi yapmak için yeterli yetkiniz yok.",
"category-disabled": "Kategori aktif değil",
"topic-locked": "Başlık Kilitli",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Bu sohbet mesajı zaten silinmiş.",
"chat-restored-already": "Bu sohbet mesajı zaten geri yüklendi.",
"chat-room-does-not-exist": "Sohbet Odası Mevcut Değil",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Bu gönderi için zaten oy verdin.",
"reputation-system-disabled": "İtibar sistemi devre dışı.",
"downvoting-disabled": "Eksi oylama devre dışı bırakılmış. ",

View File

@@ -62,6 +62,7 @@
"no-user": "Користувач не існує",
"no-teaser": "Тизер не існує",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "У вас недостатньо повноважень для цієї дії. ",
"category-disabled": "Категорію відключено",
"topic-locked": "Тему заблоковано",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Це повідомлення чату вже було видалено.",
"chat-restored-already": "Це чат повідомлення вже було відновлене",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Ви вже проголосували за цей пост.",
"reputation-system-disabled": "Система репутацій вимкнена.",
"downvoting-disabled": "Голосування проти вимкнено",

View File

@@ -62,6 +62,7 @@
"no-user": "Người dùng không tồn tại",
"no-teaser": "Đoạn giới thiệu không tồn tại",
"no-flag": "Cờ không tồn tại",
"no-chat-room": "Chat room does not exist",
"no-privileges": "Bạn không đủ quyền để thực thi hành động này",
"category-disabled": "Chuyên mục bị khóa",
"topic-locked": "Chủ đề bị khóa",
@@ -156,6 +157,9 @@
"chat-deleted-already": "Cuộc trò chuyện này đã được xóa.",
"chat-restored-already": "Tin nhắn trò chuyện này đã được khôi phục.",
"chat-room-does-not-exist": "Phòng trò chuyện không tồn tại.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "Bạn đã bỏ phiếu cho bài viết này",
"reputation-system-disabled": "Hệ thống đánh giá uy tính đã bị vô hiệu hóa.",
"downvoting-disabled": "Phản đối đã bị tắt",

View File

@@ -62,6 +62,7 @@
"no-user": "用户不存在",
"no-teaser": "主题预览不存在",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "您没有权限执行此操作。",
"category-disabled": "版块已禁用",
"topic-locked": "主题已锁定",
@@ -156,6 +157,9 @@
"chat-deleted-already": "聊天消息已经被删除",
"chat-restored-already": "此聊天消息已经恢复。\n",
"chat-room-does-not-exist": "聊天室不存在。",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "您已为此帖回复投过票了。",
"reputation-system-disabled": "声望系统已禁用。",
"downvoting-disabled": "踩已被禁用",

View File

@@ -62,6 +62,7 @@
"no-user": "使用者不存在",
"no-teaser": "主題預覽不存在",
"no-flag": "Flag does not exist",
"no-chat-room": "Chat room does not exist",
"no-privileges": "您的權限不足以執行此操作。",
"category-disabled": "版面已停用",
"topic-locked": "主題已鎖定",
@@ -156,6 +157,9 @@
"chat-deleted-already": "聊天訊息已經被刪除",
"chat-restored-already": "此聊天訊息已經恢復。",
"chat-room-does-not-exist": "Chat room does not exist.",
"cant-add-users-to-chat-room": "Can't add users to chat room.",
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
"chat-room-name-too-long": "Chat room name too long.",
"already-voting-for-this-post": "您已讚過此貼文回覆了。",
"reputation-system-disabled": "聲望系統已停用。",
"downvoting-disabled": "倒讚已被停用",

View File

@@ -265,6 +265,9 @@ TopicObjectSlim:
name:
type: string
description: The topic thumbnail filename
path:
type: string
description: Path to topic thumbnail without upload_url prefix
url:
type: string
description: Relative path to the topic thumbnail

View File

@@ -19,6 +19,15 @@ post:
content:
type: string
example: This is the test topic's content
timestamp:
type: number
description: |
A UNIX timestamp of the topic's creation date (i.e. when it will be posted).
Specifically, this value can only be set to a value in the future if the calling user has the `topics:schedule` privilege for the passed-in category.
Otherwise, the current date and time are always assumed.
In some scenarios (e.g. forum migrations), you may want to backdate topics and posts.
Please see [this Developer FAQ topic](https://community.nodebb.org/topic/16983/how-can-i-backdate-topics-and-posts-for-migration-purposes) for more information.
example: 556084800000
tags:
type: array
items:

View File

@@ -46,8 +46,6 @@ post:
content:
type: string
example: This is a test reply
timestamp:
type: number
toPid:
type: number
required:

View File

@@ -31,6 +31,8 @@ get:
type: string
name:
type: string
path:
type: string
url:
type: string
description: Path to a topic thumbnail
@@ -155,6 +157,8 @@ delete:
type: string
name:
type: string
path:
type: string
url:
type: string
description: Path to a topic thumbnail

View File

@@ -38,4 +38,4 @@ put:
$ref: ../../../../components/schemas/Status.yaml#/Status
response:
type: object
properties: {}
properties: {}

View File

@@ -9,15 +9,23 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
configureEmailTester();
configureEmailEditor();
handleDigestHourChange();
handleSmtpServiceChange();
$(window).on('action:admin.settingsLoaded action:admin.settingsSaved', handleDigestHourChange);
$(window).on('action:admin.settingsSaved', function () {
socket.emit('admin.user.restartJobs');
});
$('[id="email:smtpTransport:service"]').change(handleSmtpServiceChange);
$(window).off('action:admin.settingsLoaded', onSettingsLoaded)
.on('action:admin.settingsLoaded', onSettingsLoaded);
$(window).off('action:admin.settingsSaved', onSettingsSaved)
.on('action:admin.settingsSaved', onSettingsSaved);
};
function onSettingsLoaded() {
handleDigestHourChange();
handleSmtpServiceChange();
}
function onSettingsSaved() {
handleDigestHourChange();
socket.emit('admin.user.restartJobs');
}
function configureEmailTester() {
$('button[data-action="email.test"]').off('click').on('click', function () {
socket.emit('admin.email.test', { template: $('#test-email').val() }, function (err) {
@@ -106,20 +114,26 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
}
function handleSmtpServiceChange() {
const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp';
$('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom);
const enabledEl = document.getElementById('email:smtpTransport:enabled');
if (enabledEl) {
if (!enabledEl.checked) {
enabledEl.closest('label').classList.toggle('is-checked', true);
enabledEl.checked = true;
alerts.alert({
message: '[[admin/settings/email:smtp-transport.auto-enable-toast]]',
timeout: 5000,
});
}
function toggleCustomService() {
const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp';
$('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom);
}
toggleCustomService();
$('[id="email:smtpTransport:service"]').change(function () {
toggleCustomService();
const enabledEl = document.getElementById('email:smtpTransport:enabled');
if (enabledEl) {
if (!enabledEl.checked) {
$('label[for="email:smtpTransport:enabled"]').toggleClass('is-checked', true);
enabledEl.checked = true;
alerts.alert({
message: '[[admin/settings/email:smtp-transport.auto-enable-toast]]',
timeout: 5000,
});
}
}
});
}
return module;

View File

@@ -77,6 +77,7 @@ define('forum/account/edit/password', [
ajaxify.go('user/' + ajaxify.data.userslug + '/edit');
}
})
.catch(alerts.error)
.finally(() => {
btn.removeClass('disabled').find('i').addClass('hide');
currentPassword.val('');

View File

@@ -88,7 +88,11 @@ define('forum/topic', [
});
}
mousetrap.bind('j', () => {
mousetrap.bind('j', (e) => {
if (e.target.classList.contains('mousetrap')) {
return;
}
const index = navigator.getIndex();
const count = navigator.getCount();
if (index === count) {
@@ -98,7 +102,11 @@ define('forum/topic', [
navigator.scrollToIndex(index, true, 0);
});
mousetrap.bind('k', () => {
mousetrap.bind('k', (e) => {
if (e.target.classList.contains('mousetrap')) {
return;
}
const index = navigator.getIndex();
if (index === 1) {
return;

View File

@@ -41,7 +41,7 @@ define('forum/topic/postTools', [
const pid = postEl.attr('data-pid');
const index = parseInt(postEl.attr('data-index'), 10);
socket.emit('posts.loadPostTools', { pid: pid, cid: ajaxify.data.cid }, async (err, data) => {
socket.emit('posts.loadPostTools', { pid: pid }, async (err, data) => {
if (err) {
return alerts.error(err);
}

View File

@@ -15,6 +15,9 @@ app = window.app || {};
reconnectionDelay: config.reconnectionDelay,
transports: config.socketioTransports,
path: config.relative_path + '/socket.io',
query: {
_csrf: config.csrf_token,
},
};
window.socket = io(config.websocketAddress, ioParams);

View File

@@ -443,6 +443,10 @@ usersAPI.changePicture = async (caller, data) => {
};
usersAPI.generateExport = async (caller, { uid, type }) => {
const validTypes = ['profile', 'posts', 'uploads'];
if (!validTypes.includes(type)) {
throw new Error('[[error:invalid-data]]');
}
const count = await db.incrObjectField('locks', `export:${uid}${type}`);
if (count > 1) {
throw new Error('[[error:already-exporting]]');

View File

@@ -95,11 +95,9 @@ module.exports = function (Categories) {
await privileges.categories.give(result.modPrivileges, category.cid, ['administrators', 'Global Moderators']);
await privileges.categories.give(result.guestPrivileges, category.cid, ['guests', 'spiders']);
cache.del([
'categories:cid',
`cid:${parentCid}:children`,
`cid:${parentCid}:children:all`,
]);
cache.del('categories:cid');
await clearParentCategoryCache(parentCid);
if (data.cloneFromCid && parseInt(data.cloneFromCid, 10)) {
category = await Categories.copySettingsFrom(data.cloneFromCid, category.cid, !data.parentCid);
}
@@ -112,6 +110,22 @@ module.exports = function (Categories) {
return category;
};
async function clearParentCategoryCache(parentCid) {
while (parseInt(parentCid, 10) >= 0) {
cache.del([
`cid:${parentCid}:children`,
`cid:${parentCid}:children:all`,
]);
if (parseInt(parentCid, 10) === 0) {
return;
}
// clear all the way to root
// eslint-disable-next-line no-await-in-loop
parentCid = await Categories.getCategoryField(parentCid, 'parentCid');
}
}
async function duplicateCategoriesChildren(parentCid, cid, uid) {
let children = await Categories.getChildren([cid], uid);
if (!children.length) {

View File

@@ -5,7 +5,6 @@ const _ = require('lodash');
const db = require('../database');
const user = require('../user');
const groups = require('../groups');
const plugins = require('../plugins');
const privileges = require('../privileges');
const cache = require('../cache');
@@ -99,39 +98,7 @@ Categories.getModerators = async function (cid) {
};
Categories.getModeratorUids = async function (cids) {
// Only check active categories
const disabled = (await Categories.getCategoriesFields(cids, ['disabled'])).map(obj => obj.disabled);
// cids = cids.filter((_, idx) => !disabled[idx]);
const groupNames = cids.reduce((memo, cid) => {
memo.push(`cid:${cid}:privileges:moderate`);
memo.push(`cid:${cid}:privileges:groups:moderate`);
return memo;
}, []);
const memberSets = await groups.getMembersOfGroups(groupNames);
// Every other set is actually a list of user groups, not uids, so convert those to members
const sets = memberSets.reduce((memo, set, idx) => {
if (idx % 2) {
memo.groupNames.push(set);
} else {
memo.uids.push(set);
}
return memo;
}, { groupNames: [], uids: [] });
const uniqGroups = _.uniq(_.flatten(sets.groupNames));
const groupUids = await groups.getMembersOfGroups(uniqGroups);
const map = _.zipObject(uniqGroups, groupUids);
const moderatorUids = cids.map((cid, index) => {
if (disabled[index]) {
return [];
}
return _.uniq(sets.uids[index].concat(_.flatten(sets.groupNames[index].map(g => map[g]))));
});
return moderatorUids;
return await privileges.categories.getUidsWithPrivilege(cids, 'moderate');
};
Categories.getCategories = async function (cids, uid) {

View File

@@ -32,12 +32,6 @@ try {
if (!semver.satisfies(version, defaultPackage.dependencies[packageName])) {
const e = new TypeError(`Incorrect dependency version: ${packageName}`);
e.code = 'DEP_WRONG_VERSION';
// delete the module from require cache so it doesn't break rest of the upgrade
// https://github.com/NodeBB/NodeBB/issues/11173
const resolvedModule = require.resolve(packageName);
if (require.cache[resolvedModule]) {
delete require.cache[resolvedModule];
}
throw e;
}
};
@@ -57,6 +51,16 @@ try {
packageInstall.preserveExtraneousPlugins();
packageInstall.installAll();
// delete the module from require cache so it doesn't break rest of the upgrade
// https://github.com/NodeBB/NodeBB/issues/11173
const packages = ['nconf', 'async', 'commander', 'chalk', 'lodash', 'lru-cache'];
packages.forEach((packageName) => {
const resolvedModule = require.resolve(packageName);
if (require.cache[resolvedModule]) {
delete require.cache[resolvedModule];
}
});
const chalk = require('chalk');
console.log(`${chalk.green('OK')}\n`);
} else {

View File

@@ -45,10 +45,11 @@ notificationsController.get = async function (req, res, next) {
{ separator: true },
]).concat(filters.moderatorFilters);
}
const selectedFilter = allFilters.find((filterData) => {
allFilters.forEach((filterData) => {
filterData.selected = filterData.filter === filter;
return filterData.selected;
});
const selectedFilter = allFilters.find(filterData => filterData.selected);
if (!selectedFilter) {
return next();
}

View File

@@ -9,6 +9,7 @@ const categories = require('../categories');
const plugins = require('../plugins');
const translator = require('../translator');
const languages = require('../languages');
const { generateToken } = require('../middleware/csrf');
const apiController = module.exports;
@@ -64,7 +65,7 @@ apiController.loadConfig = async function (req) {
'cache-buster': meta.config['cache-buster'] || '',
topicPostSort: meta.config.topicPostSort || 'oldest_to_newest',
categoryTopicSort: meta.config.categoryTopicSort || 'newest_to_oldest',
csrf_token: req.uid >= 0 && req.csrfToken && req.csrfToken(),
csrf_token: req.uid >= 0 ? generateToken(req) : undefined,
searchEnabled: plugins.hooks.hasListeners('filter:search.query'),
searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles',
bootswatchSkin: meta.config.bootswatchSkin || '',

View File

@@ -339,7 +339,7 @@ authenticationController.doLogin = async function (req, uid) {
return;
}
const loginAsync = util.promisify(req.login).bind(req);
await loginAsync({ uid: uid }, { keepSessionInfo: req.res.locals !== false });
await loginAsync({ uid: uid }, { keepSessionInfo: req.res.locals.reroll !== false });
await authenticationController.onSuccessfulLogin(req, uid);
};
@@ -383,7 +383,7 @@ authenticationController.onSuccessfulLogin = async function (req, uid) {
}),
user.auth.addSession(uid, req.sessionID),
user.updateLastOnlineTime(uid),
user.updateOnlineUsers(uid),
user.onUserOnline(uid, Date.now()),
analytics.increment('logins'),
db.incrObjectFieldBy('global', 'loginCount', 1),
]);

View File

@@ -1,6 +1,9 @@
'use strict';
const _ = require('lodash');
const user = require('../user');
const groups = require('../groups');
const posts = require('../posts');
const flags = require('../flags');
const analytics = require('../analytics');
@@ -110,7 +113,6 @@ modsController.flags.detail = async function (req, res, next) {
isAdminOrGlobalMod: user.isAdminOrGlobalMod(req.uid),
moderatedCids: user.getModeratedCids(req.uid),
flagData: flags.get(req.params.flagId),
assignees: user.getAdminsandGlobalModsandModerators(),
privileges: Promise.all(['global', 'admin'].map(async type => privileges[type].get(req.uid))),
});
results.privileges = { ...results.privileges[0], ...results.privileges[1] };
@@ -119,6 +121,28 @@ modsController.flags.detail = async function (req, res, next) {
return next(); // 404
}
async function getAssignees(flagData) {
let uids = [];
const [admins, globalMods] = await Promise.all([
groups.getMembers('administrators', 0, -1),
groups.getMembers('Global Moderators', 0, -1),
]);
if (flagData.type === 'user') {
uids = await privileges.admin.getUidsWithPrivilege('admin:users');
uids = _.uniq(admins.concat(uids));
} else if (flagData.type === 'post') {
const cid = await posts.getCidByPid(flagData.targetId);
uids = _.uniq(admins.concat(globalMods));
if (cid) {
const modUids = (await privileges.categories.getUidsWithPrivilege([cid], 'moderate'))[0];
uids = _.uniq(uids.concat(modUids));
}
}
const userData = await user.getUsersData(uids);
return userData.filter(u => u && u.userslug);
}
const assignees = await getAssignees(results.flagData);
results.flagData.history = results.isAdminOrGlobalMod ? (await flags.getHistory(req.params.flagId)) : null;
if (results.flagData.type === 'user') {
@@ -128,7 +152,7 @@ modsController.flags.detail = async function (req, res, next) {
}
res.render('flags/detail', Object.assign(results.flagData, {
assignees: results.assignees,
assignees: assignees,
type_bool: ['post', 'user', 'empty'].reduce((memo, cur) => {
if (cur !== 'empty') {
memo[cur] = results.flagData.type === cur && (

View File

@@ -26,7 +26,7 @@ module.exports = function (module) {
async function getSortedSetUnion(params) {
if (!Array.isArray(params.sets) || !params.sets.length) {
return;
return [];
}
let limit = params.stop - params.start + 1;
if (limit <= 0) {

View File

@@ -32,6 +32,9 @@ SELECT COUNT(DISTINCT z."value") c
async function getSortedSetUnion(params) {
const { sets } = params;
if (!sets || !sets.length) {
return [];
}
const start = params.hasOwnProperty('start') ? params.start : 0;
const stop = params.hasOwnProperty('stop') ? params.stop : -1;
let weights = params.weights || [];

View File

@@ -40,14 +40,24 @@ async function linkModules() {
await Promise.all(Object.keys(modules).map(async (relPath) => {
const srcPath = path.join(__dirname, '../../', modules[relPath]);
const destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
const destDir = path.dirname(destPath);
const [stats] = await Promise.all([
fs.promises.stat(srcPath),
mkdirp(path.dirname(destPath)),
mkdirp(destDir),
]);
if (stats.isDirectory()) {
await file.linkDirs(srcPath, destPath, true);
} else {
await fs.promises.copyFile(srcPath, destPath);
// Get the relative path to the destination directory
const relPath = path.relative(destDir, srcPath)
// and convert to a posix path
.split(path.sep).join(path.posix.sep);
// Instead of copying file, create a new file re-exporting it
// This way, imports in modules are resolved correctly
await fs.promises.writeFile(destPath, `module.exports = require('${relPath}');`);
}
}));
}

View File

@@ -108,7 +108,7 @@ Themes.set = async (data) => {
await db.sortedSetAdd('plugins:active', numPlugins, data.id);
} else if (!activePluginsConfig.includes(data.id)) {
// This prevents changing theme when configuration doesn't include it, but allows it otherwise
winston.error('When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP');
winston.error(`When defining active plugins in configuration, changing themes requires adding the theme '${data.id}' to the list of active plugins before updating it in the ACP`);
throw new Error('[[error:theme-not-set-in-configuration]]');
}

26
src/middleware/csrf.js Normal file
View File

@@ -0,0 +1,26 @@
'use strict';
const { csrfSync } = require('csrf-sync');
const {
generateToken,
csrfSynchronisedProtection,
isRequestValid,
} = csrfSync({
getTokenFromRequest: (req) => {
if (req.headers['x-csrf-token']) {
return req.headers['x-csrf-token'];
} else if (req.body && req.body.csrf_token) {
return req.body.csrf_token;
} else if (req.query) {
return req.query._csrf;
}
},
size: 64,
});
module.exports = {
generateToken,
csrfSynchronisedProtection,
isRequestValid,
};

View File

@@ -2,11 +2,11 @@
const async = require('async');
const path = require('path');
const csrf = require('csurf');
const validator = require('validator');
const nconf = require('nconf');
const toobusy = require('toobusy-js');
const util = require('util');
const { csrfSynchronisedProtection } = require('./csrf');
const plugins = require('../plugins');
const meta = require('../meta');
@@ -34,7 +34,7 @@ middleware.regexes = {
timestampedUpload: /^\d+-.+$/,
};
const csrfMiddleware = csrf();
const csrfMiddleware = csrfSynchronisedProtection;
middleware.applyCSRF = function (req, res, next) {
if (req.uid >= 0) {
@@ -102,11 +102,20 @@ middleware.pluginHooks = helpers.try(async (req, res, next) => {
});
middleware.validateFiles = function validateFiles(req, res, next) {
if (!Array.isArray(req.files.files) || !req.files.files.length) {
if (!req.files.files) {
return next(new Error(['[[error:invalid-files]]']));
}
next();
if (Array.isArray(req.files.files) && req.files.files.length) {
return next();
}
if (typeof req.files.files === 'object') {
req.files.files = [req.files.files];
return next();
}
return next(new Error(['[[error:invalid-files]]']));
};
middleware.prepareAPI = function prepareAPI(req, res, next) {

View File

@@ -37,7 +37,7 @@ module.exports = function (middleware) {
const loginAsync = util.promisify(req.login).bind(req);
await loginAsync(user, { keepSessionInfo: true });
await controllers.authentication.onSuccessfulLogin(req, user.uid);
req.uid = user.uid;
req.uid = parseInt(user.uid, 10);
req.loggedIn = req.uid > 0;
return true;
}

View File

@@ -211,3 +211,8 @@ privsAdmin.groupPrivileges = async function (groupName) {
const groupPrivilegeList = await privsAdmin.getGroupPrivilegeList();
return await helpers.userOrGroupPrivileges(0, groupName, groupPrivilegeList);
};
privsAdmin.getUidsWithPrivilege = async function (privilege) {
const uidsByCid = await helpers.getUidsWithPrivilege([0], privilege);
return uidsByCid[0];
};

View File

@@ -218,3 +218,7 @@ privsCategories.groupPrivileges = async function (cid, groupName) {
const groupPrivilegeList = await privsCategories.getGroupPrivilegeList();
return await helpers.userOrGroupPrivileges(cid, groupName, groupPrivilegeList);
};
privsCategories.getUidsWithPrivilege = async function (cids, privilege) {
return await helpers.getUidsWithPrivilege(cids, privilege);
};

View File

@@ -134,3 +134,8 @@ privsGlobal.groupPrivileges = async function (groupName) {
const groupPrivilegeList = await privsGlobal.getGroupPrivilegeList();
return await helpers.userOrGroupPrivileges(0, groupName, groupPrivilegeList);
};
privsGlobal.getUidsWithPrivilege = async function (privilege) {
const uidsByCid = await helpers.getUidsWithPrivilege([0], privilege);
return uidsByCid[0];
};

View File

@@ -6,6 +6,7 @@ const validator = require('validator');
const groups = require('../groups');
const user = require('../user');
const categories = require('../categories');
const plugins = require('../plugins');
const translator = require('../translator');
@@ -189,4 +190,38 @@ helpers.userOrGroupPrivileges = async function (cid, uidOrGroup, privilegeList)
return _.zipObject(privilegeList, isMembers);
};
helpers.getUidsWithPrivilege = async (cids, privilege) => {
const disabled = (await categories.getCategoriesFields(cids, ['disabled'])).map(obj => obj.disabled);
const groupNames = cids.reduce((memo, cid) => {
memo.push(`cid:${cid}:privileges:${privilege}`);
memo.push(`cid:${cid}:privileges:groups:${privilege}`);
return memo;
}, []);
const memberSets = await groups.getMembersOfGroups(groupNames);
// Every other set is actually a list of user groups, not uids, so convert those to members
const sets = memberSets.reduce((memo, set, idx) => {
if (idx % 2) {
memo.groupNames.push(set);
} else {
memo.uids.push(set);
}
return memo;
}, { groupNames: [], uids: [] });
const uniqGroups = _.uniq(_.flatten(sets.groupNames));
const groupUids = await groups.getMembersOfGroups(uniqGroups);
const map = _.zipObject(uniqGroups, groupUids);
const uidsByCid = cids.map((cid, index) => {
if (disabled[index]) {
return [];
}
return _.uniq(sets.uids[index].concat(_.flatten(sets.groupNames[index].map(g => map[g]))));
});
return uidsByCid;
};
require('../promisify')(helpers);

View File

@@ -10,6 +10,7 @@ const meta = require('../meta');
const controllers = require('../controllers');
const helpers = require('../controllers/helpers');
const plugins = require('../plugins');
const { generateToken } = require('../middleware/csrf');
let loginStrategies = [];
@@ -108,7 +109,7 @@ Auth.reloadRoutes = async function (params) {
};
if (strategy.checkState !== false) {
req.session.ssoState = req.csrfToken && req.csrfToken();
req.session.ssoState = generateToken(req, true);
opts.state = req.session.ssoState;
}
@@ -154,7 +155,7 @@ Auth.reloadRoutes = async function (params) {
}, Auth.middleware.validateAuth, (req, res, next) => {
async.waterfall([
async.apply(req.login.bind(req), res.locals.user, { keepSessionInfo: true }),
async.apply(controllers.authentication.onSuccessfulLogin, req, req.uid),
async.apply(controllers.authentication.onSuccessfulLogin, req, res.locals.user.uid),
], (err) => {
if (err) {
return next(err);

View File

@@ -9,11 +9,12 @@ const topics = require('../topics');
const user = require('../user');
const categories = require('../categories');
const meta = require('../meta');
const helpers = require('../controllers/helpers');
const controllerHelpers = require('../controllers/helpers');
const privileges = require('../privileges');
const db = require('../database');
const utils = require('../utils');
const controllers404 = require('../controllers/404');
const routeHelpers = require('./helpers');
const terms = {
daily: 'day',
@@ -23,18 +24,18 @@ const terms = {
};
module.exports = function (app, middleware) {
app.get('/topic/:topic_id.rss', middleware.maintenanceMode, generateForTopic);
app.get('/category/:category_id.rss', middleware.maintenanceMode, generateForCategory);
app.get('/topics.rss', middleware.maintenanceMode, generateForTopics);
app.get('/recent.rss', middleware.maintenanceMode, generateForRecent);
app.get('/top.rss', middleware.maintenanceMode, generateForTop);
app.get('/top/:term.rss', middleware.maintenanceMode, generateForTop);
app.get('/popular.rss', middleware.maintenanceMode, generateForPopular);
app.get('/popular/:term.rss', middleware.maintenanceMode, generateForPopular);
app.get('/recentposts.rss', middleware.maintenanceMode, generateForRecentPosts);
app.get('/category/:category_id/recentposts.rss', middleware.maintenanceMode, generateForCategoryRecentPosts);
app.get('/user/:userslug/topics.rss', middleware.maintenanceMode, generateForUserTopics);
app.get('/tags/:tag.rss', middleware.maintenanceMode, generateForTag);
app.get('/topic/:topic_id.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTopic));
app.get('/category/:category_id.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForCategory));
app.get('/topics.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTopics));
app.get('/recent.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForRecent));
app.get('/top.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTop));
app.get('/top/:term.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTop));
app.get('/popular.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForPopular));
app.get('/popular/:term.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForPopular));
app.get('/recentposts.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForRecentPosts));
app.get('/category/:category_id/recentposts.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForCategoryRecentPosts));
app.get('/user/:userslug/topics.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForUserTopics));
app.get('/tags/:tag.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTag));
};
async function validateTokenIfRequiresLogin(requiresLogin, cid, req, res) {
@@ -46,16 +47,16 @@ async function validateTokenIfRequiresLogin(requiresLogin, cid, req, res) {
}
if (uid <= 0 || !token) {
return helpers.notAllowed(req, res);
return controllerHelpers.notAllowed(req, res);
}
const userToken = await db.getObjectField(`user:${uid}`, 'rss_token');
if (userToken !== token) {
await user.auth.logAttempt(uid, req.ip);
return helpers.notAllowed(req, res);
return controllerHelpers.notAllowed(req, res);
}
const userPrivileges = await privileges.categories.get(cid, uid);
if (!userPrivileges.read) {
return helpers.notAllowed(req, res);
return controllerHelpers.notAllowed(req, res);
}
return true;
}
@@ -127,7 +128,7 @@ async function generateForCategory(req, res, next) {
db.getSortedSetRevIntersect({
sets: ['topics:tid', `cid:${cid}:tids:lastposttime`],
start: 0,
stop: 25,
stop: 24,
weights: [1, 0],
}),
]);
@@ -230,7 +231,7 @@ async function generateSorted(options, req, res, next) {
const { cid } = req.query;
if (cid) {
if (!await privileges.categories.can('topics:read', cid, uid)) {
return helpers.notAllowed(req, res);
return controllerHelpers.notAllowed(req, res);
}
params.cids = [cid];
}

View File

@@ -79,7 +79,11 @@ sitemap.getPages = async function () {
async function getSitemapCategories() {
const cids = await categories.getCidsByPrivilege('categories:cid', 0, 'find');
return await categories.getCategoriesFields(cids, ['slug']);
const categoryData = await categories.getCategoriesFields(cids, ['slug']);
const data = await plugins.hooks.fire('filter:sitemap.getCategories', {
categories: categoryData,
});
return data.categories;
}
sitemap.getCategories = async function () {
@@ -128,7 +132,12 @@ sitemap.getTopicPage = async function (page) {
tids = await privileges.topics.filterTids('topics:read', tids, 0);
const topicData = await topics.getTopicsFields(tids, ['tid', 'title', 'slug', 'lastposttime']);
if (!topicData.length) {
const data = await plugins.hooks.fire('filter:sitemap.getCategories', {
page: page,
topics: topicData,
});
if (!data.topics.length) {
sitemap.maps.topics[page - 1] = {
sm: '',
cacheExpireTimestamp: Date.now() + (1000 * 60 * 60 * 24),
@@ -136,7 +145,7 @@ sitemap.getTopicPage = async function (page) {
return sitemap.maps.topics[page - 1].sm;
}
topicData.forEach((topic) => {
data.topics.forEach((topic) => {
if (topic) {
topicUrls.push({
url: `${nconf.get('relative_path')}/topic/${topic.slug}`,

View File

@@ -34,13 +34,25 @@ Sockets.init = async function (server) {
}
}
io.use(authorize);
io.on('connection', onConnection);
const opts = {
transports: nconf.get('socket.io:transports') || ['polling', 'websocket'],
cookie: false,
allowRequest: (req, callback) => {
authorize(req, (err) => {
if (err) {
return callback(err);
}
const csrf = require('../middleware/csrf');
const isValid = csrf.isRequestValid({
session: req.session || {},
query: req._query,
headers: req.headers,
});
callback(null, isValid);
});
},
};
/*
* Restrict socket.io listener to cookie domain. If none is set, infer based on url.
@@ -62,7 +74,11 @@ Sockets.init = async function (server) {
};
function onConnection(socket) {
socket.ip = (socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress || '').split(',')[0];
socket.uid = socket.request.uid;
socket.ip = (
socket.request.headers['x-forwarded-for'] ||
socket.request.connection.remoteAddress || ''
).split(',')[0];
socket.request.ip = socket.ip;
logger.io_one(socket, socket.uid);
@@ -112,43 +128,49 @@ async function onMessage(socket, payload) {
return winston.warn('[socket.io] Empty payload');
}
const eventName = payload.data[0];
let eventName = payload.data[0];
const params = typeof payload.data[1] === 'function' ? {} : payload.data[1];
const callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function () {};
if (!eventName) {
return winston.warn('[socket.io] Empty method name');
}
const parts = eventName.toString().split('.');
const namespace = parts[0];
const methodToCall = parts.reduce((prev, cur) => {
if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) {
return prev[cur];
}
return null;
}, Namespaces);
if (!methodToCall || typeof methodToCall !== 'function') {
if (process.env.NODE_ENV === 'development') {
winston.warn(`[socket.io] Unrecognized message: ${eventName}`);
}
const escapedName = validator.escape(String(eventName));
return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
}
socket.previousEvents = socket.previousEvents || [];
socket.previousEvents.push(eventName);
if (socket.previousEvents.length > 20) {
socket.previousEvents.shift();
}
if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) {
winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`);
return socket.disconnect();
}
try {
if (!eventName) {
return winston.warn('[socket.io] Empty method name');
}
if (typeof eventName !== 'string') {
eventName = typeof eventName;
const escapedName = validator.escape(eventName);
return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
}
const parts = eventName.split('.');
const namespace = parts[0];
const methodToCall = parts.reduce((prev, cur) => {
if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) {
return prev[cur];
}
return null;
}, Namespaces);
if (!methodToCall || typeof methodToCall !== 'function') {
if (process.env.NODE_ENV === 'development') {
winston.warn(`[socket.io] Unrecognized message: ${eventName}`);
}
const escapedName = validator.escape(String(eventName));
return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
}
socket.previousEvents = socket.previousEvents || [];
socket.previousEvents.push(eventName);
if (socket.previousEvents.length > 20) {
socket.previousEvents.shift();
}
if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) {
winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`);
return socket.disconnect();
}
await checkMaintenance(socket);
await validateSession(socket, '[[error:revalidate-failure]]');
@@ -225,9 +247,7 @@ async function validateSession(socket, errorMsg) {
const cookieParserAsync = util.promisify((req, callback) => cookieParser(req, {}, err => callback(err)));
async function authorize(socket, callback) {
const { request } = socket;
async function authorize(request, callback) {
if (!request) {
return callback(new Error('[[error:not-authorized]]'));
}
@@ -240,15 +260,13 @@ async function authorize(socket, callback) {
});
const sessionData = await getSessionAsync(sessionId);
request.session = sessionData;
let uid = 0;
if (sessionData && sessionData.passport && sessionData.passport.user) {
request.session = sessionData;
socket.uid = parseInt(sessionData.passport.user, 10);
} else {
socket.uid = 0;
uid = parseInt(sessionData.passport.user, 10);
}
request.uid = socket.uid;
callback();
request.uid = uid;
callback(null, uid);
}
Sockets.in = function (room) {

View File

@@ -14,15 +14,15 @@ const utils = require('../../utils');
module.exports = function (SocketPosts) {
SocketPosts.loadPostTools = async function (socket, data) {
if (!data || !data.pid || !data.cid) {
if (!data || !data.pid) {
throw new Error('[[error:invalid-data]]');
}
const cid = await posts.getCidByPid(data.pid);
const results = await utils.promiseParallel({
posts: posts.getPostFields(data.pid, ['deleted', 'bookmarks', 'uid', 'ip', 'flagId']),
isAdmin: user.isAdministrator(socket.uid),
isGlobalMod: user.isGlobalModerator(socket.uid),
isModerator: user.isModerator(socket.uid, data.cid),
isModerator: user.isModerator(socket.uid, cid),
canEdit: privileges.posts.canEdit(data.pid, socket.uid),
canDelete: privileges.posts.canDelete(data.pid, socket.uid),
canPurge: privileges.posts.canPurge(data.pid, socket.uid),

View File

@@ -74,6 +74,6 @@ module.exports = function (SocketUser) {
await user.isAdminOrSelf(socket.uid, data.uid);
api.users.generateExport(socket, { type, ...data });
api.users.generateExport(socket, { type, uid: data.uid });
}
};

View File

@@ -82,9 +82,8 @@ module.exports = function (Topics) {
data.title = String(data.title).trim();
data.tags = data.tags || [];
if (data.content) {
data.content = utils.rtrim(data.content);
}
data.content = String(data.content || '').trimEnd();
Topics.checkTitle(data.title);
await Topics.validateTags(data.tags, data.cid, uid);
data.tags = await Topics.filterTags(data.tags, data.cid);
@@ -167,9 +166,8 @@ module.exports = function (Topics) {
data.cid = topicData.cid;
await guestHandleValid(data);
if (data.content) {
data.content = utils.rtrim(data.content);
}
data.content = String(data.content || '').trimEnd();
if (!data.fromQueue) {
await user.isReadyToPost(uid, data.cid);
Topics.checkContent(data.content);

View File

@@ -60,6 +60,7 @@ Scheduled.pin = async function (tid, topicData) {
};
Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) {
const mainPid = await topics.getTopicField(tid, 'mainPid');
await Promise.all([
db.sortedSetsAdd([
'topics:scheduled',
@@ -67,6 +68,7 @@ Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) {
'topics:tid',
`cid:${cid}:uid:${uid}:tids`,
], timestamp, tid),
posts.setPostField(mainPid, 'timestamp', timestamp),
shiftPostTimes(tid, timestamp),
]);
return topics.updateLastPostTimeFromLastPid(tid);
@@ -87,14 +89,15 @@ function unpin(tid, topicData) {
}
async function sendNotifications(uids, topicsData) {
const usernames = await Promise.all(uids.map(uid => user.getUserField(uid, 'username')));
const uidToUsername = Object.fromEntries(uids.map((uid, idx) => [uid, usernames[idx]]));
const userData = await user.getUsersData(uids);
const uidToUserData = Object.fromEntries(uids.map((uid, idx) => [uid, userData[idx]]));
const postsData = await posts.getPostsData(topicsData.map(({ mainPid }) => mainPid));
const postsData = await posts.getPostsData(topicsData.map(t => t && t.mainPid));
postsData.forEach((postData, idx) => {
postData.user = {};
postData.user.username = uidToUsername[postData.uid];
postData.topic = topicsData[idx];
if (postData) {
postData.user = uidToUserData[topicsData[idx].uid];
postData.topic = topicsData[idx];
}
});
return Promise.all(topicsData.map(

View File

@@ -174,7 +174,7 @@ module.exports = function (Topics) {
}
tids = await privileges.topics.filterTids('topics:read', tids, uid);
let topicData = await Topics.getTopicsFields(tids, ['uid', 'tid', 'cid']);
let topicData = await Topics.getTopicsFields(tids, ['uid', 'tid', 'cid', 'tags']);
const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean);
async function getIgnoredCids() {
@@ -192,11 +192,13 @@ module.exports = function (Topics) {
topicData = filtered;
const cids = params.cids && params.cids.map(String);
const { tags } = params;
tids = topicData.filter(t => (
t &&
t.cid &&
!isCidIgnored[t.cid] &&
(!cids || cids.includes(String(t.cid)))
(!cids || cids.includes(String(t.cid))) &&
(!tags.length || tags.every(tag => t.tags.find(topicTag => topicTag.value === tag)))
)).map(t => t.tid);
const result = await plugins.hooks.fire('filter:topics.filterSortedTids', { tids: tids, params: params });

View File

@@ -232,10 +232,15 @@ module.exports = function (Topics) {
if (!tids.length) {
return;
}
let topicsTags = await Topics.getTopicsTags(tids);
topicsTags = topicsTags.map(
topicTags => topicTags.filter(topicTag => topicTag && topicTag !== tag)
);
await db.deleteObjectFields(
tids.map(tid => `topic:${tid}`),
['tags'],
await db.setObjectBulk(
tids.map((tid, index) => ([
`topic:${tid}`, { tags: topicsTags[index].join(',') },
]))
);
});
}
@@ -287,7 +292,7 @@ module.exports = function (Topics) {
}
Topics.getTagData = async function (tags) {
if (!tags.length) {
if (!tags || !tags.length) {
return [];
}
tags.forEach((tag) => {

View File

@@ -52,6 +52,7 @@ Thumbs.get = async function (tids) {
const name = path.basename(thumb);
return hasTimestampPrefix.test(name) ? name.slice(14) : name;
})(),
path: thumb,
url: thumb.startsWith('http') ? thumb : path.posix.join(upload_url, thumb),
})));
@@ -151,6 +152,9 @@ Thumbs.delete = async function (id, relativePaths) {
Promise.all(toRemove.map(async relativePath => posts.uploads.dissociate(mainPid, relativePath.slice(1)))),
]);
}
if (toRemove.length) {
cache.del(set);
}
};
Thumbs.deleteAll = async (id) => {

View File

@@ -0,0 +1,46 @@
'use strict';
const db = require('../../database');
const batch = require('../../batch');
module.exports = {
name: 'Fix user email sorted sets',
timestamp: Date.UTC(2023, 1, 4),
method: async function () {
const { progress } = this;
const bulkRemove = [];
await batch.processSortedSet('email:uid', async (data) => {
progress.incr(data.length);
const usersData = await db.getObjects(data.map(d => `user:${d.score}`));
data.forEach((emailData, index) => {
const { score: uid, value: email } = emailData;
const userData = usersData[index];
// user no longer exists or doesn't have email set in user hash
// remove the email/uid pair from email:uid, email:sorted
if (!userData || !userData.email) {
bulkRemove.push(['email:uid', email]);
bulkRemove.push(['email:sorted', `${email.toLowerCase()}:${uid}`]);
return;
}
// user has email but doesn't match whats stored in user hash, gh#11259
if (userData.email && userData.email.toLowerCase() !== email.toLowerCase()) {
bulkRemove.push(['email:uid', email]);
bulkRemove.push(['email:sorted', `${email.toLowerCase()}:${uid}`]);
}
});
}, {
batch: 500,
withScores: true,
progress: progress,
});
await batch.processArray(bulkRemove, async (bulk) => {
await db.sortedSetRemoveBulk(bulk);
}, {
batch: 500,
});
},
};

View File

@@ -70,7 +70,9 @@ module.exports = function (User) {
let line = '';
usersData.forEach((user, index) => {
line += `${fields.map(field => user[field]).join(',')}`;
line += `${fields
.map(field => (isFinite(user[field]) ? `'${user[field]}'` : user[field]))
.join(',')}`;
if (showIps) {
userIPs = ips[index] ? ips[index].join(',') : '';
line += `,"${userIPs}"\n`;

View File

@@ -39,7 +39,7 @@ UserEmail.remove = async function (uid, sessionId) {
db.sortedSetRemove('email:uid', email.toLowerCase()),
db.sortedSetRemove('email:sorted', `${email.toLowerCase()}:${uid}`),
user.email.expireValidation(uid),
user.auth.revokeAllSessions(uid, sessionId),
sessionId ? user.auth.revokeAllSessions(uid, sessionId) : Promise.resolve(),
events.log({ type: 'email-change', email, newEmail: '' }),
]);
};
@@ -69,7 +69,7 @@ UserEmail.expireValidation = async (uid) => {
};
UserEmail.canSendValidation = async (uid, email) => {
const pending = UserEmail.isValidationPending(uid, email);
const pending = await UserEmail.isValidationPending(uid, email);
if (!pending) {
return true;
}
@@ -134,13 +134,13 @@ UserEmail.sendValidationEmail = async function (uid, options) {
await UserEmail.expireValidation(uid);
await db.set(`confirm:byUid:${uid}`, confirm_code);
await db.pexpire(`confirm:byUid:${uid}`, emailConfirmExpiry * 24 * 60 * 60 * 1000);
await db.pexpire(`confirm:byUid:${uid}`, emailConfirmExpiry * 60 * 60 * 1000);
await db.setObject(`confirm:${confirm_code}`, {
email: options.email.toLowerCase(),
uid: uid,
});
await db.pexpire(`confirm:${confirm_code}`, emailConfirmExpiry * 24 * 60 * 60 * 1000);
await db.pexpire(`confirm:${confirm_code}`, emailConfirmExpiry * 60 * 60 * 1000);
winston.verbose(`[user/email] Validation email for uid ${uid} sent to ${options.email}`);
events.log({
@@ -196,6 +196,20 @@ UserEmail.confirmByUid = async function (uid) {
throw new Error('[[error:invalid-email]]');
}
// If another uid has the same email throw error
const oldUid = await db.sortedSetScore('email:uid', currentEmail.toLowerCase());
if (oldUid && oldUid !== parseInt(uid, 10)) {
throw new Error('[[error:email-taken]]');
}
const confirmedEmails = await db.getSortedSetRangeByScore(`email:uid`, 0, -1, uid, uid);
if (confirmedEmails.length) {
// remove old email of user by uid
await db.sortedSetsRemoveRangeByScore([`email:uid`], uid, uid);
await db.sortedSetRemoveBulk(
confirmedEmails.map(email => [`email:sorted`, `${email.toLowerCase()}:${uid}`])
);
}
await Promise.all([
db.sortedSetAddBulk([
['email:uid', uid, currentEmail.toLowerCase()],

View File

@@ -40,8 +40,13 @@ Interstitials.email = async (data) => {
issuePasswordChallenge: !!data.userData.uid && hasPassword,
},
callback: async (userData, formData) => {
if (formData.email) {
formData.email = String(formData.email).trim();
}
// Validate and send email confirmation
if (userData.uid) {
const isSelf = parseInt(userData.uid, 10) === parseInt(data.req.uid, 10);
const [isPasswordCorrect, canEdit, { email: current, 'email:confirmed': confirmed }, { allowed, error }] = await Promise.all([
user.isPasswordCorrect(userData.uid, formData.password, data.req.ip),
privileges.users.canEdit(data.req.uid, userData.uid),
@@ -68,13 +73,17 @@ Interstitials.email = async (data) => {
if (formData.email === current) {
if (confirmed) {
throw new Error('[[error:email-nochange]]');
} else if (await user.email.canSendValidation(userData.uid, current)) {
} else if (!await user.email.canSendValidation(userData.uid, current)) {
throw new Error(`[[error:confirm-email-already-sent, ${meta.config.emailConfirmInterval}]]`);
}
}
// Admins editing will auto-confirm, unless editing their own email
if (isAdminOrGlobalMod && userData.uid !== data.req.uid) {
if (!await user.email.available(formData.email)) {
throw new Error('[[error:email-taken]]');
}
await user.email.remove(userData.uid);
await user.setUserField(userData.uid, 'email', formData.email);
await user.email.confirmByUid(userData.uid);
} else if (canEdit) {
@@ -99,8 +108,8 @@ Interstitials.email = async (data) => {
}
if (current.length && (!hasPassword || (hasPassword && isPasswordCorrect) || isAdminOrGlobalMod)) {
// User explicitly clearing their email
await user.email.remove(userData.uid, data.req.session.id);
// User or admin explicitly clearing their email
await user.email.remove(userData.uid, isSelf ? data.req.session.id : null);
}
}
} else {

View File

@@ -27,9 +27,13 @@ module.exports = function (User) {
if (now - parseInt(userOnlineTime, 10) < 300000) {
return;
}
await db.sortedSetAdd('users:online', now, uid);
await User.onUserOnline(uid, now);
topics.pushUnreadCount(uid);
plugins.hooks.fire('action:user.online', { uid: uid, timestamp: now });
};
User.onUserOnline = async (uid, timestamp) => {
await db.sortedSetAdd('users:online', timestamp, uid);
plugins.hooks.fire('action:user.online', { uid, timestamp });
};
User.isOnline = async function (uid) {

View File

@@ -17,7 +17,7 @@
{{{ end }}}
{{{ each topics }}}
<tr>
<td><a href="{config.relative_path}/topics/{../slug}">{../title}</a></td>
<td><a href="{config.relative_path}/topic/{../slug}">{../title}</a></td>
<td>[[topic:posted_by, {../user.username}]]</td>
<td><span class="timeago" data-title="{../timestampISO}"></span></td>
</tr>

View File

@@ -150,7 +150,7 @@
[[admin/settings/email:smtp-transport.gmail-warning2]]
</p>
</div>
<div class="form-group well" id="email:smtpTransport:custom-service" style="display: none">
<div class="form-group well" id="email:smtpTransport:custom-service">
<h5>Custom Service</h5>
<label for="email:smtpTransport:host">[[admin/settings/email:smtp-transport.host]]</label>

View File

@@ -3,7 +3,7 @@
<div class="alert alert-info">[[modules:thumbs.modal.no-thumbs]]</div>
{{{ end }}}
{{{ each thumbs }}}
<div class="media" data-id="{./id}" data-path="{./url}">
<div class="media" data-id="{./id}" data-path="{./path}">
<div class="media-left">
<img class="media-object" src="{./url}" alt="" />
</div>

View File

@@ -12,13 +12,22 @@ const db = require('./mocks/databasemock');
const user = require('../src/user');
const utils = require('../src/utils');
const meta = require('../src/meta');
const plugins = require('../src/plugins');
const privileges = require('../src/privileges');
const helpers = require('./helpers');
describe('authentication', () => {
const jar = request.jar();
let regularUid;
const dummyEmailerHook = async (data) => {};
before((done) => {
// Attach an emailer hook so related requests do not error
plugins.hooks.register('authentication-test', {
hook: 'filter:email.send',
method: dummyEmailerHook,
});
user.create({ username: 'regular', password: 'regularpwd', email: 'regular@nodebb.org' }, (err, uid) => {
assert.ifError(err);
regularUid = uid;
@@ -27,6 +36,10 @@ describe('authentication', () => {
});
});
after(() => {
plugins.hooks.unregister('authentication-test', 'filter:email.send');
});
it('should allow login with email for uid 1', async () => {
const oldValue = meta.config.allowLoginWith;
meta.config.allowLoginWith = 'username-email';

Some files were not shown because too many files have changed in this diff Show More