Commit Graph
2738 Commits
Author SHA1 Message Date
Joffrey JAFFEUX 24875a45d5 FIX: set_notifications missing guardian topic visibility check (#37786) 2026-02-13 09:09:17 +01:00
Joffrey JAFFEUX f6d86a2718 FIX: bulk action missing report_type format validation (#37787) 2026-02-13 09:08:48 +01:00
Joffrey JAFFEUX 31f5185577 FIX: Add server-side trust level check for delete all posts (#37773)
The original commit
https://github.com/discourse/discourse/commit/ce2ca19d added a
frontend-only restriction to hide the "delete all posts" option when penalizing users at trust level 2 or above. However, this restriction was not enforced on the server side.

This is not security as it's mostly a safeguard and not a security feature. The same user could delete posts one by one if they wanted to.
2026-02-13 09:07:18 +01:00
Sam e75f8b7730 DEV: minor api cleanup, unhandled args (#37795)
if ip address has an invalid action you get the right error
2026-02-13 17:44:10 +11:00
Penar Musaraj 8eaf428ee2 DEV: Show unauthorized message when OAuth::Unauthorized is hit (#37776) 2026-02-12 13:41:33 -05:00
Joffrey JAFFEUX 50e774b49d FIX: enforces login for create in user emails controller (#37770)
There's no significant security issue as this will get caught by a
guardian later and the rate limiting is scoped to the remote ip, so an
attacker can't rate limit a victim and prevent them to use this
endpoint.
2026-02-12 17:10:43 +01:00
Natalie Tay f939c4ebac FIX: Tag search 500 when content_localization is enabled (#37769)
With `content_localization_enabled`, searching for tags via any tag
dropdowns (or hashtag autocompletes) triggers a 500 error. The crash
occurs because a preloader is called on a non-AR result returned by
`DiscourseTagging.filter_allowed_tags` :sadpikachu:, which are plain
structs, not AR Tags instances. Additionally, `tag_counts_json` calls AR
methods (which access `.locale` that don't exist on the minisql objects.

This PR fixes the issue by converting mini results to Tag ARs with
`.includes(:localizations)` before passing them to tag_counts_json.
 
- TagsController#search
- TagHashtagDataSource#search
- TagHashtagDataSource#search_without_term
2026-02-12 23:58:03 +08:00
Régis Hanol 1a2f9f311e FIX: Use unaccent() for category name/slug search (#37622)
Categories with accented names (e.g. "Éditions") were not appearing in
search results, category chooser dropdowns, or hashtag autocomplete when
searching with unaccented terms (e.g. "editions").

PostgreSQL's `ILIKE` and `LOWER()` are case-insensitive but not
accent-insensitive, so queries like `name ILIKE '%editions%'` would not
match "Éditions".

This wraps all category name/slug comparisons with PostgreSQL's
`unaccent()` function across:
- `/categories/search` endpoint
- `/categories/hierarchical_search` endpoint
- Hashtag (#) autocomplete
- `category:` and `#slug` search filters

Introduces `Category.normalize_sql(expr)` helper that wraps SQL
expressions with `lower(unaccent(...))` to centralize the normalization
logic and make it easier to extend in the future.

On the frontend, adds a shared `removeAccents()` utility using NFD
normalization for client-side `Category.search()` and search filter
suggestions.

Ref - https://meta.discourse.org/t/395355
2026-02-12 08:43:23 +01:00
Krzysztof Kotlarek f065202715 FIX: Include tag slug in tag groups search response (#37751)
What is the problem?

The tag groups search endpoint (`TagGroupsController#search`) returns
tag data with only `id` and `name` fields. The `slug` field is missing
from the response, which can cause issues on the client side when it
needs the slug to build URLs or match tags correctly — especially for
tags whose slug differs from their name.

What is the solution?

Add `slug` to the `pluck` and `map` calls in
`TagGroupsController#search` so the response includes the tag slug
alongside `id` and `name`.
2026-02-12 15:06:56 +08:00
Sam 84b59e80fb DEV: redeliver_event looks up WebHookEvent globally without scoping to a specific web_hook (#37706)
This is an admin only feature, we had not validation that the webhook
event we were trying to redeliver was part of the webhook

This cleans this up and makes code more consistent
2026-02-12 10:10:25 +11:00
Joffrey JAFFEUX 6f14e93e09 FIX: Missing requires_login — authentication relies solely on guardian call (#37716)
## Summary

`ExportCsvController` lacks `requires_login`, causing anonymous requests
to `POST /export_csv/export_entity.json` to receive a misleading 422
"rate limit" error instead of a proper 403 "not logged in" response.

Guardians were blocking any exploit so there was no security exploit.

## Source

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

---

🤖 Generated via [Patch Triage](https://patch.discourse.org/patch-triage)
2026-02-11 12:25:58 +01:00
Joffrey JAFFEUX 4a2f917d33 FIX: update and create_multiple actions missing from requires_login (#37715)
## Summary

`update` and `create_multiple` actions in `InvitesController` were
missing from the `requires_login only:` list, allowing unauthenticated
requests to bypass the `ensure_logged_in` check and reach the action
body.

It's not marked as a security commit as guardians were preventing any
exploit.

## Source

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

---

🤖 Generated via [Patch Triage](https://patch.discourse.org/patch-triage)
2026-02-11 12:21:32 +01:00
Joffrey JAFFEUX b3242f6852 FIX: correct Discourse.InvalidParameters usage (#37690)
We were using `Discourse.InvalidParameters(...)` instead of
`Discourse::InvalidParameters.new(...)` which would raise a
`NoMethodError` (500) instead of the correct invalid parameters error.
This codepath was not tested and this commit adds a test for it.
2026-02-11 10:28:12 +01:00
Natalie TayandKrzysztof Kotlarek 737577e8b4 DEV: Move canonical tag routes to /tag/slug/id keeping /tag/name support (#37055)
Switches tag URLs from name-based (`/tag/my-tag`) to slug+id-based
(`/tag/my-tag/123`), making tag references stable across renames and
enabling translated tag names.

  | Type | Before | After |
  |------|--------|-------|
  | Browser | `/tag/my-tag` | `/tag/my-tag/123` |
  | Browser | `/tag/my-tag/l/latest` | `/tag/my-tag/123/l/latest` |
  | API | `/tag/my-tag.json` | `/tag/123.json` |

- Old `/tag/:name` URLs still work
- Browser requests get 301 redirected to canonical URLs
- API (JSON) requests return data without redirect
-  Untagged (`/tag/none`) and intersection routes remain unchanged.


Depends on https://github.com/discourse/discourse/pull/36678

---------

Co-authored-by: Krzysztof Kotlarek <kotlarek.krzysztof@gmail.com>
2026-02-11 10:21:19 +08:00
Joffrey JAFFEUX 1d2d2f04c9 FIX: remove_bookmarks missing from requires_login (#37676)
## Summary

The `remove_bookmarks` action in `TopicsController` was missing from the
`requires_login` list, allowing anonymous users to hit the endpoint and
trigger a 500 error (NoMethodError on nil) instead of receiving a proper
403 response. There's no security concern as we would after check for a
current_user, but it's much cleaner to fail early with a clean error.

## Source

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

---

🤖 Generated via [Patch Triage](https://patch.discourse.org/patch-triage)
2026-02-10 15:56:07 +01:00
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