Commit Graph
2723 Commits
Author SHA1 Message Date
Joffrey JAFFEUX dc51fcabcb FIX: raise 404 when sidebar section doesn't exist (#37675)
## Summary

`SidebarSectionsController#update` and `#destroy` return 403 Forbidden
instead of 404 Not Found when given a non-existent section ID.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/195
- Original Commit:
https://github.com/discourse/discourse/blob/main/app/controllers/sidebar_sections_controller.rb

---

🤖 Generated via [Patch Triage](https://patch.discourse.org/patch-triage)
2026-02-10 15:47:49 +01:00
Natalie Tay 8a79c788a5 FIX: Do not write localized fancy title to db when fancy_title is null (#37668)
Related:
https://meta.discourse.org/t/yet-another-title-localization-issue/395469

### bug context

tldr; `Topic#fancy_title` saves a fancy_title to db when the fancy_title
is null.

This bug requires a certain incantation to trigger.
- Topic 395465 exists with `title = "Notification level button always
says \"tracking\""` and
`fancy_title = NULL` in the DB
  - A `TopicLocalization` exists for this topic in `zh_CN`
  - `content_localization_enabled` is on

When a crawler hits `GET /t/.../395465?tl=zh_CN`, localization
replacement happens on the topic, which writes the title attribute, so
the state is now
  - `title` = Chinese (modified)
  - `fancy_title` = NULL (untouched, still what was loaded from DB)
  
When serializing via `TopicViewSerializer` which uses
`LocalizedFancyTopicTitleMixin`, we call `topic.fancy_title`. The
`topic#fancy_title` generates the fancy_title from the title value, then
writes the chinese fancy title to db 😢


https://github.com/discourse/discourse/blob/e935ed63b28a30ee7ae6a7783ae05fe33edf3367/app/models/topic.rb#L532-L545

### fix

This commit fixes the issue by ensuring the fancy_title is always written
along with the title, preventing the need for invoking
`topic#fancy_title`.
2026-02-10 22:22:07 +08:00
Joffrey JAFFEUX 9694183f7c FIX: enforces logged in, in badges actions (#37666)
There was no actual security issue due to guardian methods, but it's a
better high level check to ensure user is logged in first.
2026-02-10 12:00:56 +01:00
Joffrey JAFFEUX 51c4266c35 FIX: user_count uses public_send with user-controlled input on SiteSetting (#37660)
## Summary

The `user_count` action in `Admin::SiteSettingsController` uses
`public_send` with user-controlled input, allowing invocation of
arbitrary ActiveRecord methods (like `default_scopes`) on `SiteSetting`
that start with `default_`.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/240
- Original Commit:
https://github.com/discourse/discourse/blob/main/app/controllers/admin/site_settings_controller.rb

---

🤖 Generated via [Patch Triage](https://patch.discourse.org/patch-triage)
2026-02-10 10:14:15 +01:00
Sam e26bc210a6 FIX: Log Discourse ID setting changes to staff action logs (#37649)
## Summary

The original commit added a new Discourse ID settings page with the
ability to enable/disable Discourse ID and regenerate credentials.
However, the `update_settings` action used direct assignment to
`SiteSetting.enable_discourse_id` which bypasses the staff action
logging mechanism, meaning changes to this setting were not being
recorded in the admin logs.
2026-02-10 18:18:35 +11:00
Sam 9e2d4f14d9 DEV: Missing requires_login for various actions (#37650)
General hygiene, we should require login from state changing routes and
not allow anon to end up getting an incorrect error message here.
2026-02-10 18:18:20 +11:00
Martin Brennan 8125ffa60a FIX: Stop impersonation session not working with group-based upcoming change (#37655)
Followup 42da6860fd

When the upcoming change for "Impersonate without logout" was added, we
didn't take into account that the "stop impersonation" action would not
work if the Staff enabled option for the upcoming change was used.

This was happening because in the ImpersonateController#destroy action
we werecheckingif  the current user had `impersonate_without_logout`
enabled, but we should be checking if the acting user had that
permission instead (i.e. the original admin not the user they are
impersonating)

c.f.
https://meta.discourse.org/t/new-bug-with-experimental-impersonation-interface/395621
2026-02-10 16:24:31 +10:00
Martin Brennan 6e8570b0fb DEV: Rename experimental_ upcoming change settings (#37589)
We can use the status of upcoming changes to indicate
whether they are experimental or not, having experimental
in the setting name is redundant.

Migrates the settings and the upcoming change events,
updates code, and updates yaml translation keys.
2026-02-10 10:34:37 +10:00
Sam decf64f0d7 DEV: toggle_anon missing from requires_login (#37644)
The `toggle_anon` action in `UsersController` was missing from the
`requires_login` filter,
2026-02-10 08:36:27 +11:00
Régis Hanol a1cc7a2ba5 DEV: Add topic_count to UserSerializer staff attributes (#37632)
The UserSerializer exposes post_count as a staff attribute but does not
expose topic_count, even though the underlying data is available on the
UserStat model. This means consumers like the Discourse AI tool API's
discourse.getUser() function can retrieve how many replies a user has
made but not how many topics they have created, giving an incomplete
picture of user activity.

Add topic_count as a staff attribute in UserSerializer, mirroring the
existing post_count implementation. Also add spec coverage for all four
staff attributes (post_count, topic_count, can_be_deleted,
can_delete_all_posts) to verify they are included for staff scopes and
excluded for non-staff scopes.

Ref: https://meta.discourse.org/t/394878
2026-02-09 19:30:20 +01:00
Natalie Tay 6cae7a3a7e DEV: Remove tag_id param in favour of tag param for topic creation with tags to allow tag creation (#37597)
In https://github.com/discourse/discourse/pull/36678 we introduced the
`tag_ids` param for topic creation and updating to add tags to a topic,
however, this does not cater for new tags which are allowed based on the
UI and previous API.

Furthermore, the frontend wasn't even using the new param, making the
new `tag_ids` implementation on the controller moot.

This commit 
- removes this `tag_id` param
- updates the deprecation note to the `tags` param to require `[{ id: 1,
name: "old1" }, { name: "new1" }]` rather than the old `["old1",
"new1"]`.
- including IDs here eliminates the case where John renames `"old1"` to
`"old2"`, but Mary sends `["old1", "new1"]`, thus creating the `"old1"`
tag again when it was already renamed.
  - keeps us consistent with other endpoints that take in `tags`
  
Reviewer note: The `tag_topic_by_names` method is [extremely
complicated](https://github.com/discourse/discourse/blob/2b24fc91c1f46e57913d6acb7aee0c05041f64be/lib/discourse_tagging.rb#L19-L248).
Ideally, we would like to have a `tag_topics` method that does not incur
that many `Tag` queries, but to prevent a large refactor here, we are
re-using `tag_topic_by_names`.
2026-02-09 14:48:58 +08:00
Alan Guo Xiang Tan 8750ae2e18 FIX: allow new tags to be created from the Tag Group editor (#37594)
What is the problem?

When creating or editing a tag group, users can type new tag names into
the `TagChooser` (which has `allowAny=true`). The select-kit component
assigns a string-based ID to these new tags. `TagGroupsController#tag_groups_params`
blindly extracts `id` from each tag object, producing the string name
instead of a database ID for new tags. `TagGroup#tag_ids=` then silently
ignores these invalid values, so the new tags are dropped without error.
The same issue affects parent tags.

This was introduced in 9e99066b07 which changed tags from string arrays
to object arrays but did not handle the case where new tags have no
numeric ID.

What is the solution?

On the frontend, the `TagGroupsForm#save` action now strips non-numeric
IDs from tags and parent tags before sending to the backend via a private
`#serializeTag` method. Tags with a numeric `id` (existing) keep both
`id` and `name`; tags with a string `id` (new) are sent with only `name`.

On the backend, `TagGroupsController#tag_groups_params` now splits the
tags array into existing (have `id`) and new (no `id`). New tag names are
passed to `DiscourseTagging.find_or_create_tags!` which validates names via
`tags_for_saving` and creates them. The same logic applies to parent
tags.
2026-02-06 14:27:01 +08:00
Martin BrennanandLoïc Guitaut 43ccc4def0 FIX: Logging hole in group user histories and convert group create to service (#37054)
When creating groups, we weren't logging the group owner
as being the owner in group histories, nor were we logging
them being added to the group. So it was not clear in the user
logs where this user ever came from. This PR fixes the issue,
and also converts the admin group create endpoint to use a
service.

Now the logs look like this when a group is created:

<img width="830" height="378" alt="image"
src="https://github.com/user-attachments/assets/871e319a-5512-411e-8565-3b63803e688c"
/>


c.f.
https://meta.discourse.org/t/logging-hole-for-group-histories/392942

---------

Co-authored-by: Loïc Guitaut <loic@discourse.org>
2026-02-06 13:34:58 +10:00
Kris fb26fc66a4 FIX: support nested descriptions in object settings (#37538)
Reported here:
https://meta.discourse.org/t/labels-and-descriptions-missing-from-nested-object-settings/394685

Descriptions for nested object settings were not being displayed due to
a mismatch in locale key formatting. Stripping `.schema.properties.` so
that the locale keys from the serializer match the keys expected in the
template fixes it

Before: 
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/1e0ab4c6-e6eb-478d-97bc-76f16739135b"
/>


After: 
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/a9a9e64c-4efa-4807-94a1-dd853ffd9f04"
/>
2026-02-05 14:01:58 -05:00
Osama Sayegh 47ca25d56e FIX: Eager load base_scheme color_scheme_colors in themes controller (#37550)
When a color scheme has a base_scheme, the base scheme's
color_scheme_colors were not being eager loaded, causing strict loading
violations.

This updates the includes statement to properly load the nested
association `base_scheme: :color_scheme_colors` instead of just
`:base_scheme`.

Also adds a test that creates color schemes with base_scheme
relationships to verify the associations are properly loaded without
strict loading errors.
2026-02-05 13:17:56 +03:00
Natalie Tay 73e3286356 FIX: Cache banners per locale (#37561)
Currently topic banners are localizable, but unfortunately there is a
global cache that is not scoped per locale.

This commit fixes that.
2026-02-05 17:21:12 +08:00
Sam d9c414e645 FIX: Add missing error handling to merge_topic hijack block (#37542)
The original commit moved the topic merging operation into a `hijack`
block to avoid Unicorn worker timeouts when merging topics with many
posts. However, it omitted error handling that exists in the similar
`move_posts` method, which could lead to 500 errors with empty bodies in
production when `ActiveRecord::RecordInvalid` or
`ActiveRecord::RecordNotSaved` exceptions occur.

Without this we will get no information about why merging did not work
2026-02-05 17:46:16 +11:00
Natalie Tay bc570b3751 FIX: Accept booleans for whisper, is_warning, no_bump, shared_draft when creating a post (#37551)
Due to the following,


https://github.com/discourse/discourse/blob/e0d8ba788e957787a1cb06f856c9c15253c4eb9e/app/controllers/posts_controller.rb#L956-L963

we currently only check for `whisper == "true"` and not `whisper ==
true` when creating a post. This also applies for the attributes
is_warning, no_bump, shared_draft.

This commit ensures that booleans and strings `true` are accepted.
2026-02-05 14:45:04 +08:00
Natalie Tay 2d7f8062fa FIX: User preferences page for tracking tags should show tag name (#37517)
Reported here:
https://meta.discourse.org/t/tag-ids-displaying-instead-of-slugs/395070

The fix ensures that we return the names and not just the IDs
2026-02-04 15:47:50 +08:00
Alan Guo Xiang Tan 02fd694014 UX: Count all reviewables on user profile flags counter (#37402)
What is the problem?

The flagged posts count on user profiles only shows
`ReviewableFlaggedPost` items
and excludes other reviewable types like `ReviewableQueuedPost`,
`ReviewableUser`,
and plugin-provided reviewables (chat, AI, etc.).

`User#number_of_flagged_posts` queries only `ReviewableFlaggedPost`
records, and
the review queue link includes `type="ReviewableFlaggedPost"` as a query
parameter
to scope results. This has been confusing for staff who expect the count
to reflect
all pending review items for a user, not just flagged posts.

What is the solution?

Product has decided to change the label from "X flagged posts" to "X
flags" and
stop scoping to just `ReviewableFlaggedPost`, so the counter reflects
all
reviewables for the user.

1. Rename `User#number_of_flagged_posts` to `User#number_of_flags` and
query
   all `Reviewable` records instead of just `ReviewableFlaggedPost`
2. Remove `type="ReviewableFlaggedPost"` from the review queue link
query params
so clicking through shows all reviewables matching the username filter
3. Rename the translation key from `flagged_posts` to `flags` and CSS
class from
   `.flagged-posts` to `.flags`
2026-02-04 14:04:57 +08:00
Jarek Radosz 844bd89719 FIX: Add a page limit to directory_items endpoint (#37496) 2026-02-03 20:02:44 +01:00
Keegan George ec42f4cfaa FIX: typos, dead code, debug statements, incorrect naming, etc. (#37462)
## 🔍 Overview

This update ensures that we fix some typos in the codebase, along with a broken CSS selector, accidental debug statements, and invalid references.
2026-02-02 16:12:33 -08:00
Sam a1c2ac845d DEV: fix a large amount of typos (#37428) 2026-02-02 16:31:58 +11:00
Natalie Tay 9e99066b07 DEV: Expand top_tags, topic.tags, etc, to return an array of tag objects instead of tag names (#36678)
Currently in several endpoints, we return an array of strings for tags.
Our goal with this PR is to expand array tag name strings to an array of
tag objects.

#### before: Tags were returned as string arrays
```
{ "tags": ["support", "bug-report"] }
```

#### after: Tags are returned as object arrays
```
{ "tags": [{"id": 12, "name": "support", "slug": "support"}, {"id": 13, "name": "bug-report", "slug": "bug-report"}] }
```

This allows us to start referencing tags by their ids, and return more
information for a tag for future features.

This commit involves updating several areas:
- topic lists (/latest.json, /top.json, /c/:category/:id.json, etc, for
`top_tags`)
- tag chooser components (`MiniTagChooser`, `TagDrop`, etc)
- topic view (/t/:id.json)
- tag groups (/tag_groups.json, tags, parent_tag)
- category settings
- staff action logs
- synonyms
- ...

APIs that reference tags based on their names will still be supported
with a deprecation warning. Moving on, we will reference them using
their tag ids.
2026-02-02 10:03:02 +08:00
Martin Brennan 42da6860fd DEV: Migrate existing experiments to upcoming changes (#37401)
- **DEV: Migrate experimental_impersonation to upcoming changes**
- **DEV: Migrate experimental_form_templates to upcoming changes**
- **DEV: Migrate experimental_auto_grid_images to upcoming changes**
2026-02-02 11:08:12 +10:00
Nat 9c0642a2e7 SECURITY: Download allowlist for uploaded files 2026-01-28 17:11:14 +00:00
Penar Musaraj e4dc4c3b85 SECURITY: Add maximum length limit for new_username param 2026-01-28 17:11:14 +00:00
Nat 357febfb47 DEV: Better protection for drafts
Starting with rate limit on endpoints
2026-01-28 17:11:14 +00:00
Nat bcd5a7ae04 SECURITY: Ensure moderator can see post/topic before allowing them to change owner 2026-01-28 17:11:14 +00:00
David Battersby 9e088bc3c7 SECURITY: Add guardian check on PM to topic conversion 2026-01-28 17:11:14 +00:00
Régis Hanol 9892628a50 SECURITY: Restrict staff action logs visibility for moderators
Previously, moderators had full access to all staff action logs, which
exposed sensitive information including webhook secrets, API keys, site
settings, private messages, and restricted categories.

This change implements an allowlist approach where moderators can only
see actions relevant to their role (user management, posts, topics,
badges, etc.) while admin-only actions (site settings, webhooks, API
keys, themes, etc.) are hidden.

Additionally, content-level redaction ensures moderators cannot see
details of logs referencing private topics, restricted categories, or
deleted content they don't have access to.

Site setting gates control visibility of category, trust level, and
email actions based on existing moderator permission settings.

Ref - t/171137
2026-01-28 17:11:14 +00:00
zogstrip 5e99b52007 FEATURE: Add admin-only reports and centralize report visibility logic
Introduces a `Report.hidden?` class method that consolidates all report
visibility checks into a single location. This replaces duplicated
conditional logic that was scattered across the controller and query
classes.

The new method handles:
- Admin-only reports (e.g., `top_uploads`) that moderators cannot access
- Legacy pageview report visibility based on `use_legacy_pageviews` setting

Previously, the controller's `#bulk` and `#show` actions each had their
own inline checks for hidden reports, and `Reports::ListQuery` duplicated
this logic again. Now all three locations delegate to `Report.hidden?`,
making the visibility rules easier to maintain and extend.

To prevent accidental privilege escalation, the `admin:` keyword argument
is required with no default value. A forgotten parameter now raises an
`ArgumentError` rather than silently granting admin access. This parameter
flows from `current_user.admin?` in both the reports controller and the
admin search controller through to the query and model, ensuring
consistent access control.

Ref - t/171141
2026-01-28 17:11:14 +00:00
Régis Hanol 250c54e302 SECURITY: prevent permalink redirects from leaking restricted slugs
Permalinks pointing to access-restricted resources (private topics,
categories, posts, or hidden tags) were redirecting users to URLs
containing the resource slug, even when the user didn't have access.

This leaked potentially sensitive information (e.g., private topic
titles) via the redirect Location header and the 404 page's search box.

This fix adds access checks via a new `PermalinkGuardian` module before
redirecting or returning target URLs. If the current user cannot see
the target resource, a 404 is returned instead.

Also fixes `Guardian#can_see_tag?` to properly check hidden tag
visibility instead of always returning true.

Ref - t/172554
2026-01-28 17:11:14 +00:00
Sam ca3cdbb18c FIX: Strip HTML from category og:description meta tag (#37322)
Use `description_text` instead of `description` for the category
meta description to ensure HTML tags are stripped from the
og:description and twitter:description meta tags.
2026-01-27 18:16:35 +11:00
Alan Guo Xiang Tan 7d3e965ae5 PERF: Respect guardian when calculating topics per page in categories (#37259)
**What is the problem?**

`CategoriesController.topics_per_page` counted all top-level categories
regardless of user permissions. On sites with many restricted
categories, this caused excessive topic fetching for users who could
only see a small number of categories.

**What is the solution?**

Use `Category.secured(guardian)` to only count categories visible to the
current user and add a maximum cap of 100 topics as a safety net.
2026-01-22 14:17:36 +08:00
Krzysztof Kotlarek 831c11c2fc DEV: Verify crawler view includes post body content (#37206)
Add test coverage to ensure crawler-rendered HTML includes post cooked
content. This helps catch regressions where crawler views might show
topic titles but miss post bodies, which can negatively impact SEO and
search engine indexing.
2026-01-21 11:41:26 +08:00
fb9bb31983 FEATURE: Notify admins of upcoming changes and log events (#37003)
This commit adds several pieces of functionality to help keep admins
in the loop about upcoming changes.

First of all, there is a new initializer on boot that will notify admins
about
newly available upcoming changes, as well as log removed changes and
status movement of existing changes.

* When there is a new upcoming change, we only notify admins about
   it when the status is the `promote_upcoming_changes_on_status` - 1,
   e.g. if `promote_upcoming_changes_on_status` is `beta` then we only
   tell admin about the change once it has reached `alpha`. This means
   we may log the `added` event in one deploy, but only actually notify
   admins in a subsequent deploy.
* We log removed upcoming changes so we can automatically delete old
   site setting data in a future job as needed.

We also now  notify  admins when  upcoming changes are automatically
promoted to enabled based on the site's
`promote_upcoming_changes_on_status`:

<img width="378" height="600" alt="image"
src="https://github.com/user-attachments/assets/4200fbee-9990-4bbc-a378-85946e631e77"
/>

In addition, we now show an indicator in the admin sidebar
if there are new upcoming changes that have been added since
they last visited the upcoming change config page. This data
is stored in a user custom field, because Redis is ephemeral,
and storing in the User table is overkill because 99% of users
are not staff:

<img width="248" height="112" alt="image"
src="https://github.com/user-attachments/assets/4c3d3cf7-ac39-45f8-a2c8-a049cb85b8e9"
/>

Finally, this commit moves both the Track and Promote initializer
logic behind a `DistributedMutex`, we don't want multiple processes
running the same logic here, it needs to be only once.

---------

Co-authored-by: Loïc Guitaut <loic@discourse.org>
Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
2026-01-21 12:45:54 +10:00
Penar MusarajandKeegan George 3f17a7ee90 DEV: Add site setting to allow/disallow email invites (#37215)
This is default enabled, but some admins can opt to disable email invites.

When disabled, users can still create invites, but only copy/paste links.

Co-authored-by: Keegan George <kgeorge13@gmail.com>
2026-01-20 15:02:04 -05:00
Penar Musaraj e47c03d223 DEV: Do not show auth_redirect note for discourse://auth_redirect (#37212)
This is an internal protocol used by the Discourse mobile app, and
showing the note with host/port info is not useful in this case.
2026-01-20 09:29:38 -05:00
Régis Hanol 7764fc61b6 FIX: Pass site_setting_name through S3 upload paths (#37209)
Follow-up to #37005.

Non-image file uploads for site settings (like llms_txt) were failing
with "Sorry, you can only upload images for this setting" when using S3
storage. This happened because the external upload completion endpoints
weren't passing site_setting_name to the upload validator.

Without site_setting_name, the validator can't look up the setting's
authorized_extensions and falls back to allowing only images.

The direct upload path (UploadsController#create) already handled this
correctly - this ensures the S3 paths match that behavior:

- complete-external-upload: now extracts and passes site_setting_name
- complete-multipart: now sends for_site_setting and site_setting_name
from the frontend (for consistency, though site settings don't currently
use multipart uploads)
2026-01-20 12:52:08 +01:00
Régis Hanol c668871f46 FEATURE: Add native support for /llms.txt (#36939)
Adds the ability to serve a custom llms.txt file at /llms.txt, allowing
site admins to provide information for LLM crawlers about the site's
structure and important content.

The implementation uses a new `llms_txt` upload site setting where
admins can upload a .txt or .md file (max 512KB). The file is served via the
StaticController, following the same pattern as favicon.

For external storage (S3), the content is cached in Redis using the
upload's SHA1 as the cache key - this means the cache automatically
invalidates when a new file is uploaded. For local storage, the file
is served directly via send_file.

Blocked crawlers are allowed to access /llms.txt, similar to robots.txt.

Ref - t/162690
Requires - https://github.com/discourse/discourse/pull/36939
2026-01-20 08:53:20 +01:00
Loïc Guitaut 3ac5a0fa5d FIX: Allow to delete first posts with bulk destroy (#37180)
When selecting various posts from the search UI to be deleted using the
bulk actions, an error can be encountered saying, "You are not permitted
to view the requested resource". That’s because it doesn’t work when the
post is the first post of a topic.

This patch addresses the issue by checking if the user can either
destroy the post or the related topic. The logic for destroying a topic
is the same as the one for destroying a post since in both cases we’re
calling `PostDestroyer#destroy`.
2026-01-19 11:39:38 +01:00
Régis Hanol 23982658b2 FIX: Improve error message when updating group notification settings (#37184)
When updating a group's notification defaults (like
watching_category_ids) via API, if existing members would be affected,
the server returns a 422 asking the caller to specify whether to apply
changes to existing users.

The previous error message was confusing: "You supplied invalid
parameters to the request: update_existing_users" - even though the
caller hadn't supplied that parameter at all. The new message clearly
explains what's happening and what to do:

"This change affects X existing group members. You must specify the
'update_existing_users' parameter (true or false) to indicate whether to
apply the new notification defaults to existing members."

Also fixed a bug where the confirmation modal state wasn't being reset
when navigating between groups, causing the modal to not appear on
subsequent group edits. The modal now returns its result directly
instead of storing it in component state.

Ref - https://meta.discourse.org/t/-/393572
2026-01-19 15:08:38 +11:00
David TaylorandJarek Radosz d7de0f6735 DEV: Refactor locale bundle loading (#37114)
Previously, locale bundles were using `require()` directly, and were
very sensitive to load order. This made it very hard to refactor things,
especially for our upcoming move to Vite.

This commit updates the four types of locale bundle to contain only very
simple JS code, with no dependencies at the top level. They simply add
POJO and functions to the `window._discourse_locale_data` global. When
`discourse-i18n` is loaded, it checks that global, loads up the data,
and executes the functions (passing in any dependencies like
messageformat/runtime where required).

Changes in the HTML files are to bring the locale bundles up to the
first position, since they no longer have any dependencies, and we need
to be 100% sure they're loaded before discourse-i18n.

Tests are updated for the new bundle format, and the most complex ones
have been converted to system specs, so that we no longer need to create
a mock environment for executing messageformat/discourse-i18n/etc.

`lib/deprecated` changes (and associated pretty-text changes) are to
make the dependencies more formalized, because `require()` is subject to
race conditions, which started being hit following the locale refactor.

---------

Co-authored-by: Jarek Radosz <jradosz@gmail.com>
2026-01-16 11:45:14 +00:00
Alan Guo Xiang Tan 35f660e3a1 DEV: Remove unused transition_to and transition_to_id from reviewable serializer (#37135)
What is the problem?

The `ReviewablePerformResultSerializer` includes `transition_to` and
`transition_to_id` attributes in API responses, but these values are
not consumed by any frontend code. The frontend only uses
`remove_reviewable_ids`, `reviewable_count`, `unseen_reviewable_count`,
and `completed_message` from the perform result.

What is the solution?

Remove the unused `transition_to` and `transition_to_id` attributes
from `ReviewablePerformResultSerializer`, along with the
`transition_to_id` method. Update corresponding test expectations.
2026-01-15 15:04:53 +08:00
Kris 3486084812 UX: set categories_topics to 0 by default for automatic calculation (#37110)
This sets the default to `0` to gain the benefit of automatically
calculating the topic count for better page symmetry. Sites overriding
the default will see no change.

I've also updated the setting description for clarity. 

Before:
<img width="1266" height="276" alt="image"
src="https://github.com/user-attachments/assets/5e381f9b-c79c-4a8e-83a7-c2212299048b"
/>


After: 
<img width="1314" height="302" alt="image"
src="https://github.com/user-attachments/assets/b49b9c16-4c53-4601-9fe1-c8f5200139fe"
/>
2026-01-14 11:07:43 -05:00
Régis HanolandEthan M f1a8a63865 DEV: Add bypass_bump parameter to post update API (#36976)
Adds a new `bypass_bump` parameter to the post update endpoint that
allows privileged users (staff and TL4) to update a post without bumping
the topic.

The parameter can be passed as either a query param
(`?bypass_bump=true`) or in the request body (`post[bypass_bump]=true`).

When omitted, behavior is unchanged - edits to the last post in a topic
will bump it as usual.

Closes #34712

Co-authored-by: Ethan M <176145523+Ethsim12@users.noreply.github.com>
2026-01-14 15:32:47 +01:00
Régis Hanol 20e4134a09 FEATURE: Prevent silenced users from liking and using reactions (#37040)
Silenced users can now no longer like posts or use reactions, which
closes a potential griefing vector that was difficult for moderators
to monitor.

The implementation adds a silenced check to the guardian's post_can_act?
method for likes, and introduces a new can_use_reactions? guardian method
in the discourse-reactions plugin that delegates to the same logic. This
ensures both features share the same authorization path.

Additionally, silenced users' custom status is now shadow-banned: visible
to themselves and staff, but hidden from other users.

A new `can_see_user_status?` guardian method centralizes the visibility
logic, used by serializers and MessageBus publishing. Status updates
from silenced users are now only broadcast to themselves and staff.

Also includes minor CSS fixes for user status spacing and alignment.

Chat reactions already had proper silenced user checks in place via the
can_react? guardian method, so no changes were needed there.

Ref - t/140084
2026-01-13 13:59:57 +01:00
Alan Guo Xiang Tan 7d430bdadd DEV: Add tests for parent_tag_name param in TagGroupsController (#37076)
What is the problem?

The `parent_tag_name` parameter is accepted by both `#create` and
`#update` actions in `TagGroupsController`, but there are no tests
verifying this functionality works correctly.

What is the solution?

Add two test cases to verify that creating and updating tag groups
with a `parent_tag_name` parameter correctly sets the parent tag
association and returns it in the response.
2026-01-13 14:46:47 +08:00
Alan Guo Xiang Tan e3729246db DEV: Fix incorrect spec for tag groups controller update (#37075)
What is the problem?

The spec for `TagGroupsController#update` had several issues:
1. Request params were incorrectly nested (`tag_group: { tag_group:
{...} }`)
2. No assertion verified that the tags were actually updated
3. Expected subject in `UserHistory` was the old name instead of the new
name

What is the solution?

Fix the params structure, add an assertion to verify the tags are
updated
correctly, and update the expected subject to match the new tag group
name.
2026-01-13 14:46:33 +08:00