Commit Graph
63546 Commits
Author SHA1 Message Date
Joffrey JAFFEUX df0cfc4dbe DEV: code cleanup (#37809)
- Removes double private keyword
- Fixes test mutating json as string, instead of json as hash. Calling
`string["description"] = ""` uses Ruby’s `String#[]=` method which finds
the substring `description` and replaces it with `""`, corrupting the
JSON but leaving it parseable with a `nil` description field instead of
`""`.
2026-02-13 11:55:53 +01:00
Joffrey JAFFEUX f837298d1f DEV: improves integer validator (#37808)
- correctly handles undefined/blank values, and adds tests
- adds more tests for string as input
2026-02-13 11:45:54 +01:00
Joffrey JAFFEUX 2e0bdedcd6 FIX: is_custom_flag is not used anymore (#37806)
It's been mostly changed in this commit two years ago in
https://github.com/discourse/discourse/commit/c975c7fe1bc23383cdd9ae1bb90438ee7350a731
and this one has not been updated probably as the template was in a
different file at that time.
2026-02-13 11:10:54 +01:00
Natalie Tay 7691be669a DEV: Setup backend for dedicated tag settings page (#37804)
This commit adds the backend for a dedicated tag settings page (part of
#37170 split, since the PR is huge). The frontend will follow in a
separate PR.

New settings and controller actions on `TagsController` allow fetching
and updating tag settings via `/tag/:id/settings.json`. Updates are
handled by `TagSettingsUpdater` in transaction.

Some details
- TagSettingsSerializer returns raw (unlocalized) tag and synonym names
for editing
  - Tag renames are logged via StaffActionLogger
- Tag#ensure_slug now respects explicit slug changes, allowing
independent slug editing
2026-02-13 17:14:49 +08:00
Loïc Guitaut bcf33a2901 DEV: Refactor category hierarchical search (#37609)
The monolithic `CategoryHierarchicalSearch` service mixed query
building, eager loading, and pagination logic in a single class. The
query was a large raw SQL string built through conditional string
concatenation. Fragments like `#{matches_sql}`, `#{only_ids_sql}`,
`#{except_ids_sql}` were stitched together, with LIMIT/OFFSET appended
via ternary interpolation and named placeholders passed through a
manually assembled hash. This made the query fragile and hard to follow.

Break it into focused, single-responsibility classes under the
`Category::` namespace:

- `Category::HierarchicalSearch` — service orchestrator using
`Service::Base`, with a contract that owns pagination logic (page
validation, limit/offset computation)
- `Category::Query::HierarchicalSearch` — query object that uses
ActiveRecord's interface where it naturally fits (`.where()` with
parameter binding, `.limit()`, `.offset()`, `.with()`, `.joins()`) and
isolates the genuinely complex SQL (recursive CTEs, term matching) into
small named methods rather than a monolithic heredoc
- `Category::Action::EagerLoadAssociations` — extracted eager loading
into a reusable `Service::ActionBase`

The controller is simplified to a single
`Category::HierarchicalSearch.call(service_params)` call with proper
`on_success` / `on_failed_contract` / `on_failure` handling, replacing
manual param transformation and direct result access.

Specs are rewritten to test each class in isolation: the service spec
stubs its collaborators to verify orchestration, the query spec
exercises actual SQL behavior, and the action spec verifies preloading.

Service structure and spec patterns follow the [Discourse service object
guidelines](https://meta.discourse.org/t/using-service-objects-in-discourse/333641)
and the [RSpec Style Guide](https://rspec.rubystyle.guide/).
2026-02-13 09:43:56 +01:00
David Taylor 0410416c33 DEV: Drop manual bundler install (#37758)
Since bundler 2.3.0 (2021-12-21), it automatically takes care of
installing & using the version specified in `Gemfile.lock`.
2026-02-13 08:37:02 +00:00
Discourse Translator Bot 4830937c85 I18N: Update translations (#37803) 2026-02-13 09:31:47 +01:00
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
Natalie Tay 91147d8301 FIX: 404s on 'no tags' (#37793)
Currently, selecting "no tags" on the tag dropdown results in a 404 as
the "none" keyword is unhandled. This fixes the issue.



https://github.com/user-attachments/assets/04d1fe66-ee8b-461c-ad14-a0ba8e50cf3c
2026-02-13 13:54:42 +08:00
Sam 99207c0d52 FEATURE: add dedicated API scope for artifacts (#37790)
Adds dedicated scope for ai artifacts
2026-02-13 16:01:52 +11:00
Sam 79395cdc29 FIX: saved indicator showing on failed save in chat preferences (#37792)
Simplify code, only show saved when actually saved (not on save fail)
2026-02-13 12:51:34 +08:00
Alan Guo Xiang Tan 9b8c97c9fa FIX: preserve poll data when moving posts to another topic (#37791)
What is the problem?

The poll plugin does not handle the `post_moved` event fired by
`PostMover`. For normal reply moves `Post#id` is unchanged so poll
data is retained, but when moving the first post of a topic
(`PostMover` recreates it via `PostCreator#create!` in the
destination topic) or when an admin moves a reply with the "keep a
copy in the original topic" option (`PostMover#move` with
`freeze_original: true`, which duplicates the post), the new post
gets a different `Post#id` and the `Poll` records remain pointing at
the old `Post#id` via `Poll#post_id`, orphaning them along with
their associated `PollOption` and `PollVote` records.

What is the solution?

Add an `on(:post_moved)` handler in `plugins/poll/plugin.rb` that:

1. Skips processing when `Post#id` is unchanged (normal reply moves).
2. Removes empty `Poll` and `PollOption` records that the poll
   plugin's `validate_post` hook auto-creates when `PostCreator`
   rebuilds the post from raw. Deletes in FK order: `PollVote` →
   `PollOption` → `Poll`.
3. Reassigns the original polls — which carry the actual votes — to
   the new post by updating `Poll#post_id` via `Poll.update_all`.
4. Updates `DiscoursePoll::HAS_POLLS` custom field on both posts via
   `PollsUpdater.update_post_custom_fields` — sets it on the new
   post (needed for the `freeze_original` path which bypasses
   `PostCreator`) and clears it on the old post so
   `TopicView#polls` does not issue unnecessary queries.
5. Wraps steps 2-3 in an `ActiveRecord::Base.transaction`.
2026-02-13 12:30:12 +08:00
Kris a398d74242 UX: add focus indicator to radio cards (#37783)
These were missing a focus state 

Before:
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/b0e213f0-1d21-4fd8-9c87-32ec46a6c800"
/>


After: 
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/4767e5e8-4c22-48fe-a29f-8a5dc224e344"
/>
2026-02-12 18:37:28 -05:00
Gerhard Schlager d979f3573d FIX: Multiple bulk importer fixes (#37740) 2026-02-12 23:20:54 +01:00
Penar Musaraj 6242826eaf UX: Hide the allow_email_invites site setting (#37780)
It's enabled by default and disabling it is a power-user feature, best
to hide for most admins.

Internal ref t/174224
2026-02-12 13:41:48 -05:00
Penar Musaraj 8eaf428ee2 DEV: Show unauthorized message when OAuth::Unauthorized is hit (#37776) 2026-02-12 13:41:33 -05:00
Michiel Hendriks 6edaf4bc27 FIX: Correct badge argument in the admin-above-badge-buttons plugin outlet (#37775)
See the bug report:
https://meta.discourse.org/t/plugin-outlet-admin-above-badge-buttons-does-not-set-badge-argument-correctly/395940
2026-02-12 17:32:20 +00:00
Jarek Radosz 608d742404 DEV: Run release-prepare-latest-bump only on discourse/discourse (#37772) 2026-02-12 17:09:11 +00: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
Jarek Radosz 0a203f28f3 DEV: Fix/skip various flaky specs (#37762)
Extracted from #35477
2026-02-12 16:19:22 +01:00
Jarek RadoszandDavid Taylor d0b1df32a2 DEV: Move plugins' admin code to admin directories (#37761)
Extracted from #35477

---------

Co-authored-by: David Taylor <david@taylorhq.com>
2026-02-12 16:17:45 +01:00
Jarek Radosz f7371d3c44 DEV: Rename theme-compiler's entrypoint and compatModules (#37768)
Extracted from #35477
2026-02-12 16:11:43 +01:00
Joffrey JAFFEUX 18a1e0ff20 FIX: ensures we fetch the direct message channel (#37767)
`chatable_id` is not unique and can be a DM or Category, which could
potentially be a security issue if there was a category with the same ID
than a DM. Luckily, due to differences in the serializer this couldn't
cause an actual security issue but only a 500 error.

Given the specificity of this codepath, a test has been added to ensure
we never end up returning the wrong data.
2026-02-12 16:09:30 +01:00
chapoi e9a21c81af UX: update copy for typing settings (#37765)
We (no longer?) use an actual number, instead offering a text-based
select. The millisecond specification seems overkill.
2026-02-12 15:42:06 +01:00
Kris fdd9e68187 UX: improve styling for admin onboarding banner (#37741)
Makes some general style improvements, including styling for the
Foundation theme.

* Switches the columns to grid so we can use subgrid to keep titles/text
vertically aligned across columns
* Some styling for smaller screens (tablet and below, still hidden on
mobile)
* Changes button classes for Foundation to get better colors in absence
of --accent-color
* Improves position and colors for the close button 
* Max width on descriptions and `text-wrap: pretty;` for better wrapping

Before:
<img width="2248" height="530" alt="image"
src="https://github.com/user-attachments/assets/f6f7e91d-7a9f-494d-9077-d6f2dca9df50"
/>
<img width="2208" height="506" alt="image"
src="https://github.com/user-attachments/assets/8294dfb4-72e8-462c-98a1-b545f48fff55"
/>
<img width="1946" height="490" alt="image"
src="https://github.com/user-attachments/assets/73ff3262-aa1b-4f4f-b9d4-9b4e8f62d326"
/>
<img width="1972" height="468" alt="image"
src="https://github.com/user-attachments/assets/3a38c8dd-e29d-4850-9302-98f59d7f4c32"
/>


After:
<img width="2234" height="488" alt="image"
src="https://github.com/user-attachments/assets/024adf3b-66b0-434f-bf28-edd2253c3a75"
/>
<img width="2260" height="486" alt="image"
src="https://github.com/user-attachments/assets/cf8beea6-bbef-424b-b519-054262b4609a"
/>
<img width="1970" height="518" alt="image"
src="https://github.com/user-attachments/assets/3dd58a82-d862-4937-b50c-31b180a5db63"
/>
<img width="1990" height="516" alt="image"
src="https://github.com/user-attachments/assets/8d41c2b0-c596-48b9-9f3d-79043ab9a94a"
/>
2026-02-12 09:40:23 -05:00
Jarek Radosz b67cc0def6 DEV: Add ai_secrets to excluded tables in migrations (#37759) 2026-02-12 15:10:54 +01:00
Renato Atilio daef88a0fb FIX: calendar plugin event timezone mismatch (#37516) 2026-02-12 09:25:18 -03:00
Jarek Radosz e38bd1954a Revert "DEV: Upgrade bundler from 2.6.4 to 4.0.6 (latest) (#37725)" (#37756)
This reverts commit e605639033.

The upgrade will require some changes in the base discourse docker image
first.
2026-02-12 19:01:34 +08:00
Joffrey JAFFEUX 9a8b16f76a FIX: Missing guardian check on target visibility in unassign and assign (#37755)
Any user with assign permission could unassign/assign topics/posts they
cannot see (private categories, PMs they're not part of). This is
treated as a bug and not security due to the high status assigners
already have and the limited impact the action has. You would get a 200
and `{ success: true }` response which doesn't leak any important
information. So in practice it could only allow a high trusted user to
be disruptive.
2026-02-12 11:15:03 +01:00
Natalie Tay a349b9a6c1 FEATURE: Show localized tags (#37044)
Show localized tag names in several places:

- Sidebar
- Tag dropdowns
- Topic list
- Topic view

<img width="1366" height="1005" alt="Screenshot 2026-01-11 at 10 38
23 PM"
src="https://github.com/user-attachments/assets/86a1b9b3-d8a5-481f-a23b-e171b26bd70c"
/>
2026-02-12 18:06:14 +08:00
Joffrey JAFFEUX 8f0ee77a65 DEV: uses constant time to validate mailgun signature (#37754)
There was no (sanely) exploitable timing attack here due to redis cache
and rate limiting, but it's better to use the correct comparison
methods.
2026-02-12 10:49:16 +01:00
Natalie Tay 127adedf3f FIX: Redirect to the target tag when synonym is visited (#37672)
There's a bug now when navigating to a tag synonym via the tag dropdown
would show some inconsistencies in the URL and the dropdown. (URL
showing synonym, dropdown showing synonym's target). This is also
evident with the tag banners theme component.

This fix ensures that the user will only see the target tag by making
sure we route to the target immediately from the dropdown, without
triggering a refresh or redirect.
2026-02-12 17:33:02 +08:00
Discourse Translator Bot 7a42a1a2e3 I18N: Update translations (#37753) 2026-02-12 10:30:04 +01:00
Nicholas La Roux e605639033 DEV: Upgrade bundler from 2.6.4 to 4.0.6 (latest) (#37725) 2026-02-12 09:23:17 +00:00
Régis Hanol 6355a0100f FIX: Exclude watched-words and censored from ProseMirror markdown-it engine (#37687)
Watched Words (Replace) caused recursive nesting when editing posts in
the rich editor. Each edit added another layer of replacement text, e.g.
"ETA" → "Estimated Time of Arrival (ETA)" → "Estimated Time of Arrival
(Estimated Time of Arrival (ETA))".

The ProseMirror editor reuses markdown-it to parse raw markdown into its
document tree. This markdown-it engine included the `watched-words` and
`censored` features, which are rendering-only transforms meant for
cooked output. When parsing raw into the ProseMirror AST, these features
replaced text content. On save, ProseMirror serialized the AST back to
markdown with the replacements baked into the new raw — which then got
replaced again on the next cook, causing recursive nesting for `replace`
and permanent content destruction for `censor`.

Adds `watched-words` and `censored` to the omit list for the ProseMirror
markdown-it engine, alongside the already-omitted `onebox`. These
features now only run during server-side cooking where they belong.

Ref - https://meta.discourse.org/t/241735/34
2026-02-12 10:10:47 +01:00
David Battersby 8f33d1c5dd FIX: add alternate email duplicate row (#37673)
Clicking “Resend confirmation email” to an unconfirmed alternate email
address creates a duplicate row that persists after hard refresh.

### The problem

When adding an alternate (secondary) email, within
`lib/email_updater.rb` the value for `add` is true and `old_email`
should be nil — there is no "old email" being replaced, you're just
adding a new one. But the old code always passed @user.email (the
primary email) as old_email in the find_or_initialize_by query.

This caused a duplicate row because:

1. The first time a user adds an alternate email, a row is created with
old_email: nil (lib/email_updater.rb:57 sets it to nil after the
find/initialize).
2. If the user tries the same flow again (e.g. re-requesting
confirmation), find_or_initialize_by searches for a row matching
{user_id: X, old_email: "primary@example.com", new_email:
"alt@example.com"}. That doesn't match the existing row (which has
old_email: nil), so it initializes a new record instead of finding the
existing one.
3. Line 57 then sets old_email = nil on this new record, and when it's
saved, you get a duplicate row — two EmailChangeRequest records with
identical user_id, old_email: nil, and new_email.

### Solution

By passing nil directly in the find_or_initialize_by when add is true,
the query now correctly matches the existing row (old_email: nil), so it
finds the existing request instead of creating a duplicate. The
find_or_initialize_by lookup is now consistent with the value that
actually gets persisted.

Internal ref: /t/129754
2026-02-12 12:46:55 +04:00
Joffrey JAFFEUX 3b029def04 FIX: prevents destroying an unknown id to create a record (#37735)
Prior to this fix destroying a query would attempt to create a query with the provided id, hide it, and actually fail name validation. This fix simplifies the logic and just disables hiding default queries as it wasn't working anyways (the delete button will also be removed from the UI in this case) and will raise a 404 when trying to delete a non existing query.
2026-02-12 09:36:46 +01:00
small-lovely-cat f0d0075465 UX: fix reviewable select-kit covered by the next review-item (#37749)
The `review-item` lacks proper z-index, and the `select-kit` is stacked
using natural DOM order, when the former `select-kit` appears on the
latter `review-item`, it will become transparent and unclickable.

This commit fix this by adding proper z-index to components.

Before:
<img width="1919" height="920" alt="image"
src="https://github.com/user-attachments/assets/5e0f0747-3cf7-4079-917e-af70dc7db29f"
/>

<img width="407" height="812" alt="image"
src="https://github.com/user-attachments/assets/e282e51d-15f9-4e0d-b313-57ae5203bdc0"
/>


After:
<img width="1919" height="924" alt="image"
src="https://github.com/user-attachments/assets/ec47434b-1787-43a2-b1fb-83c8bcd7e4cf"
/>

<img width="413" height="815" alt="image"
src="https://github.com/user-attachments/assets/67bd0802-f725-44e7-8baa-07bb914725b8"
/>
2026-02-12 09:25:15 +01: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 6aeea64184 DEV: Stabilize category calendar color mapping specs (#37748)
Use frozen time and fixed event dates instead of `Time.now` to
make the color mapping system tests deterministic and prevent
flaky failures when events fall outside the visible calendar
range. Also update the event presence selector from
`.fc-daygrid-event-dot` to `.fc-daygrid-event`
2026-02-12 08:55:17 +08:00
Osama Sayegh 1f4bc3e6c0 FIX: Update Teams integration to use Adaptive Card (#37696)
The Message Card format that the Teams integration currently uses is no
longer supported in Teams. The new format is called Adaptive Card and
this commit switches our integration to use Adaptive Card format.
2026-02-12 02:56:01 +03:00
Régis Hanol c0f38c1a57 FIX: email fields lose focus in legacy category settings (#37669)
When `enable_simplified_category_creation` is disabled, the email fields
in category settings lose focus after every keystroke, making it
impossible to type an email address.

The legacy category editor wraps all tabs in a FormKit `<Form>`
component. The Form wrapper uses `{{#each (array @data)}}` which
destroys and recreates the entire component tree when `@data` changes
identity. Since the controller's `formData` getter returns a new object
(via `getProperties`) every time a model property changes, any two-way
binding mutation — including every keystroke in a `<TextField>` —
triggers a full teardown and rebuild of all form elements, destroying
the focused input.

This commit converts the email section inputs (`email_in`,
`email_in_allow_strangers`, `mailinglist_mirror`) from two-way model
bindings to use FormKit's data layer (`form.set` / `transientData`).
Edits to these fields no longer mutate the model, so `formData` isn't
invalidated and the form is not recreated.

This commit also passes `form` to the `category-email-in` and
`category-custom-settings` plugin outlets on the legacy page, matching
what the simplified page already does, so that plugins can adopt FormKit
as well.

https://meta.discourse.org/t/395609
2026-02-12 10:47:49 +11:00
Sam 84706bcef7 FIX: Don't decrement public_version when destroying hidden revision (#37704)
The original commit fixed an issue where adding hidden tags to a post
would incorrectly increment `public_version`, causing the edit pencil
icon to appear for regular users who couldn't access the hidden
revision. The fix correctly skips incrementing `public_version` for
hidden revisions. However, the commit introduced a bug in
`update_revision` - when a hidden revision is destroyed (e.g., by
reverting changes within the grace period), `public_version` is
incorrectly decremented even though it was never incremented for that
revision.
2026-02-12 10:38:20 +11:00
Sérgio Saquetim 241c25ae27 DEV: Merge discourse-developer-docs into core (#37732)
- Import the full `discourse/discourse-developer-docs` repository into
`docs/developer-guides/` using `git subtree add` with complete git
history preserved
- Move CI workflows (lint + publish) from the nested `.github/` to
root-level workflows scoped to `docs/developer-guides/**` path changes
- The [developer-docs repo](https://github.com/discourse/discourse-developer-docs) was archived.
2026-02-11 20:38:08 -03:00
Sam 06d833152f DEV: improve approach to removing JS from house ads (#37739)
We only really intended to remove JS so people use other patterns
this was not a security feature
2026-02-12 10:37:37 +11:00