Skip to content

FEATURE: Can edit category/host relationships for embedding#1

Open
ShashankFC wants to merge 1 commit into
rest-serializer-enhancement-prefrom
rest-serializer-enhancement-post
Open

FEATURE: Can edit category/host relationships for embedding#1
ShashankFC wants to merge 1 commit into
rest-serializer-enhancement-prefrom
rest-serializer-enhancement-post

Conversation

@ShashankFC

Copy link
Copy Markdown

Test 10

Summary by CodeRabbit

Release Notes

  • New Features

    • Added admin interface for managing embeddable hosts with per-host category assignment
    • Embeddable hosts can now be created, edited, and deleted with an associated category for embedded posts
  • Refactoring

    • Migrated embeddable hosts from site settings to a database-backed model for improved management and scalability

✏️ Tip: You can customize this high-level summary in your review settings.


Replicated from ai-code-review-evaluation/discourse-coderabbit#10

@ShashankFC

Copy link
Copy Markdown
Author

@cubic-dev-ai review this pull request

@cubic-dev-ai

cubic-dev-ai Bot commented Feb 13, 2026

Copy link
Copy Markdown

@cubic-dev-ai review this pull request

@ShashankFC I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

18 issues found across 36 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="app/assets/javascripts/admin/components/embeddable-host.js.es6">

<violation number="1" location="app/assets/javascripts/admin/components/embeddable-host.js.es6:46">
P1: Missing error handling on `destroyRecord()`. The `save()` action properly uses `.catch(popupAjaxError)` but the `delete` action does not handle promise rejection. If the server returns an error, the user gets no feedback and a stale UI state.</violation>
</file>

<file name="app/controllers/admin/embeddable_hosts_controller.rb">

<violation number="1" location="app/controllers/admin/embeddable_hosts_controller.rb:10">
P1: No nil check after lookup — if the `EmbeddableHost` is not found, `save_host(host)` will raise a `NoMethodError` on nil. Use `find` (which raises `ActiveRecord::RecordNotFound` and returns 404) or add an explicit nil guard.</violation>
</file>

<file name="spec/models/topic_spec.rb">

<violation number="1" location="spec/models/topic_spec.rb:1407">
P3: Typo: `'with an emeddable host'` should be `'with an embeddable host'`.</violation>
</file>

<file name="spec/fabricators/embeddable_host_fabricator.rb">

<violation number="1" location="spec/fabricators/embeddable_host_fabricator.rb:1">
P1: File contents are swapped: `embeddable_host_fabricator.rb` contains Category fabricators instead of EmbeddableHost fabricators. The actual `Fabricator(:embeddable_host)` is defined in `category_fabricator.rb`. These file contents should be exchanged so each fabricator file matches its model name.</violation>

<violation number="2" location="spec/fabricators/embeddable_host_fabricator.rb:23">
P1: `after_build` calls `cat.update!` on an unpersisted record, which will raise an error. `update!` requires the record to be already saved to the database. Use `after_create` instead of `after_build` so the record is persisted before calling `update!`.</violation>
</file>

<file name="app/models/embeddable_host.rb">

<violation number="1" location="app/models/embeddable_host.rb:6">
P2: Potential `NoMethodError` if `host` is nil. The `before_validation` callback calls `self.host.sub!` without a nil guard. If a record is saved without setting `host`, this will crash before validation can reject it.</violation>

<violation number="2" location="app/models/embeddable_host.rb:17">
P1: Bug: case-insensitive lookup is broken because the query parameter is not downcased. `lower(host)` lowercases the column value but the bound parameter retains its original casing from `uri.host`, so mixed-case URLs will fail to match.</violation>
</file>

<file name="app/assets/javascripts/admin/controllers/admin-embedding.js.es6">

<violation number="1" location="app/assets/javascripts/admin/controllers/admin-embedding.js.es6:6">
P1: Bug: `update({})` is called with an empty object, so no data is actually sent to the server. The embeddable hosts data should be serialized and passed. Additionally, the returned promise is ignored — the user receives no success/failure feedback.</violation>

<violation number="2" location="app/assets/javascripts/admin/controllers/admin-embedding.js.es6:15">
P1: Bug: `deleteHost` only removes the host from the local array but never deletes it on the server. If the host was previously persisted, it will reappear after page reload. Following the codebase pattern (e.g., `admin-api.js.es6` `revokeKey`), a server-side destroy call should be made before or alongside the local removal.</violation>
</file>

<file name="app/assets/javascripts/discourse/adapters/rest.js.es6">

<violation number="1" location="app/assets/javascripts/discourse/adapters/rest.js.es6:22">
P2: `String.replace` with a string pattern only replaces the **first** occurrence. Use a regex with the global flag (`/_/g`) to ensure all underscores are converted to hyphens. This currently works by coincidence (each model has only one underscore), but will silently fail for any future model name with multiple underscores.</violation>
</file>

<file name="app/controllers/admin/embedding_controller.rb">

<violation number="1" location="app/controllers/admin/embedding_controller.rb:3">
P2: `:ensure_logged_in` and `:ensure_staff` are already declared in the parent `Admin::AdminController` and will be inherited. Only `:fetch_embedding` needs to be added here.</violation>

<violation number="2" location="app/controllers/admin/embedding_controller.rb:9">
P1: Bug: `update` action does not process any parameters or persist changes — it just re-renders the current state, making it a no-op identical to `show`. The action should read permitted params, update the relevant records (e.g., `EmbeddableHost` attributes), and then render the result.</violation>
</file>

<file name="db/migrate/20150818190757_create_embeddable_hosts.rb">

<violation number="1" location="db/migrate/20150818190757_create_embeddable_hosts.rb:3">
P2: `force: true` will silently drop and recreate the `embeddable_hosts` table if it already exists, causing data loss. This is inappropriate in a migration; remove `force: true` so the migration fails cleanly if the table already exists.</violation>

<violation number="2" location="db/migrate/20150818190757_create_embeddable_hosts.rb:9">
P1: Potential `NoMethodError`: if the `embed_category` setting doesn't exist or doesn't match any category name, this query returns no rows. Calling `[0]['id']` on an empty result raises `NoMethodError` on `nil`, and the fallback on line 14 is never reached. The result should be checked for emptiness first.</violation>

<violation number="3" location="db/migrate/20150818190757_create_embeddable_hosts.rb:25">
P1: SQL injection: `h` is interpolated directly into the SQL string without escaping. If the stored host value contains a single quote, this will either break the migration or allow SQL injection. Use `connection.quote` to safely escape the value.</violation>
</file>

<file name="spec/controllers/embed_controller_spec.rb">

<violation number="1" location="spec/controllers/embed_controller_spec.rb:70">
P2: Passing full URLs to `host:` relies on implicit `before_validation` stripping. For `'https://example.com/1234'`, the path `/1234` is silently dropped, so the host stored is just `example.com`. This means the downstream test "works with a host with a path" doesn't actually verify path-based host matching—it's just matching on the bare domain. Consider passing clean hostnames directly (e.g., `host: 'example.com'`) to make the test setup explicit, or if path-based matching is intended, the model and test need to be updated to support it.</violation>
</file>

<file name="app/assets/javascripts/discourse/models/store.js.es6">

<violation number="1" location="app/assets/javascripts/discourse/models/store.js.es6:197">
P1: Missing null/undefined guard on `obj[k]` before calling `.map()`. If a property ending in `_ids` has a null or non-array value, this will throw a TypeError at runtime. Add a guard to check that `obj[k]` is an array before mapping.</violation>

<violation number="2" location="app/assets/javascripts/discourse/models/store.js.es6:200">
P1: `delete obj[k]` is unconditional in the plural branch, unlike the singular branch which only deletes when hydration succeeds. This loses the original IDs even when `_lookupSubType` returns `undefined` for entries. Also, `hydrated || []` is dead code since `.map()` always returns an array. Consider filtering out undefined results and only deleting `obj[k]` if hydration produced valid results.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

delete() {
bootbox.confirm(I18n.t('admin.embedding.confirm_delete'), (result) => {
if (result) {
this.get('host').destroyRecord().then(() => {

@cubic-dev-ai cubic-dev-ai Bot Feb 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Missing error handling on destroyRecord(). The save() action properly uses .catch(popupAjaxError) but the delete action does not handle promise rejection. If the server returns an error, the user gets no feedback and a stale UI state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/assets/javascripts/admin/components/embeddable-host.js.es6, line 46:

<comment>Missing error handling on `destroyRecord()`. The `save()` action properly uses `.catch(popupAjaxError)` but the `delete` action does not handle promise rejection. If the server returns an error, the user gets no feedback and a stale UI state.</comment>

<file context>
@@ -0,0 +1,63 @@
+    delete() {
+      bootbox.confirm(I18n.t('admin.embedding.confirm_delete'), (result) => {
+        if (result) {
+          this.get('host').destroyRecord().then(() => {
+            this.sendAction('deleteHost', this.get('host'));
+          });
</file context>
Fix with Cubic

end

def update
host = EmbeddableHost.where(id: params[:id]).first

@cubic-dev-ai cubic-dev-ai Bot Feb 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: No nil check after lookup — if the EmbeddableHost is not found, save_host(host) will raise a NoMethodError on nil. Use find (which raises ActiveRecord::RecordNotFound and returns 404) or add an explicit nil guard.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/controllers/admin/embeddable_hosts_controller.rb, line 10:

<comment>No nil check after lookup — if the `EmbeddableHost` is not found, `save_host(host)` will raise a `NoMethodError` on nil. Use `find` (which raises `ActiveRecord::RecordNotFound` and returns 404) or add an explicit nil guard.</comment>

<file context>
@@ -0,0 +1,34 @@
+  end
+
+  def update
+    host = EmbeddableHost.where(id: params[:id]).first
+    save_host(host)
+  end
</file context>
Fix with Cubic

name 'Private Category'
slug 'private'
user
after_build do |cat, transients|

@cubic-dev-ai cubic-dev-ai Bot Feb 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: after_build calls cat.update! on an unpersisted record, which will raise an error. update! requires the record to be already saved to the database. Use after_create instead of after_build so the record is persisted before calling update!.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At spec/fabricators/embeddable_host_fabricator.rb, line 23:

<comment>`after_build` calls `cat.update!` on an unpersisted record, which will raise an error. `update!` requires the record to be already saved to the database. Use `after_create` instead of `after_build` so the record is persisted before calling `update!`.</comment>

<file context>
@@ -0,0 +1,27 @@
+  name 'Private Category'
+  slug 'private'
+  user
+  after_build do |cat, transients|
+    cat.update!(read_restricted: true)
+    cat.category_groups.build(group_id: transients[:group].id, permission_type: CategoryGroup.permission_types[:full])
</file context>
Fix with Cubic

@@ -0,0 +1,27 @@
Fabricator(:category) do

@cubic-dev-ai cubic-dev-ai Bot Feb 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: File contents are swapped: embeddable_host_fabricator.rb contains Category fabricators instead of EmbeddableHost fabricators. The actual Fabricator(:embeddable_host) is defined in category_fabricator.rb. These file contents should be exchanged so each fabricator file matches its model name.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At spec/fabricators/embeddable_host_fabricator.rb, line 1:

<comment>File contents are swapped: `embeddable_host_fabricator.rb` contains Category fabricators instead of EmbeddableHost fabricators. The actual `Fabricator(:embeddable_host)` is defined in `category_fabricator.rb`. These file contents should be exchanged so each fabricator file matches its model name.</comment>

<file context>
@@ -0,0 +1,27 @@
+Fabricator(:category) do
+  name { sequence(:name) { |n| "Amazing Category #{n}" } }
+  user
</file context>
Fix with Cubic

host = uri.host
return false unless host.present?

where("lower(host) = ?", host).first

@cubic-dev-ai cubic-dev-ai Bot Feb 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Bug: case-insensitive lookup is broken because the query parameter is not downcased. lower(host) lowercases the column value but the bound parameter retains its original casing from uri.host, so mixed-case URLs will fail to match.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/models/embeddable_host.rb, line 17:

<comment>Bug: case-insensitive lookup is broken because the query parameter is not downcased. `lower(host)` lowercases the column value but the bound parameter retains its original casing from `uri.host`, so mixed-case URLs will fail to match.</comment>

<file context>
@@ -0,0 +1,24 @@
+    host = uri.host
+    return false unless host.present?
+
+    where("lower(host) = ?", host).first
+  end
+
</file context>
Fix with Cubic


basePath(store, type) {
if (ADMIN_MODELS.indexOf(type) !== -1) { return "/admin/"; }
if (ADMIN_MODELS.indexOf(type.replace('_', '-')) !== -1) { return "/admin/"; }

@cubic-dev-ai cubic-dev-ai Bot Feb 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: String.replace with a string pattern only replaces the first occurrence. Use a regex with the global flag (/_/g) to ensure all underscores are converted to hyphens. This currently works by coincidence (each model has only one underscore), but will silently fail for any future model name with multiple underscores.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/assets/javascripts/discourse/adapters/rest.js.es6, line 22:

<comment>`String.replace` with a string pattern only replaces the **first** occurrence. Use a regex with the global flag (`/_/g`) to ensure all underscores are converted to hyphens. This currently works by coincidence (each model has only one underscore), but will silently fail for any future model name with multiple underscores.</comment>

<file context>
@@ -19,7 +19,7 @@ function rethrow(error) {
 
   basePath(store, type) {
-    if (ADMIN_MODELS.indexOf(type) !== -1) { return "/admin/"; }
+    if (ADMIN_MODELS.indexOf(type.replace('_', '-')) !== -1) { return "/admin/"; }
     return "/";
   },
</file context>
Suggested change
if (ADMIN_MODELS.indexOf(type.replace('_', '-')) !== -1) { return "/admin/"; }
if (ADMIN_MODELS.indexOf(type.replace(/_/g, '-')) !== -1) { return "/admin/"; }
Fix with Cubic

@@ -0,0 +1,21 @@
class Admin::EmbeddingController < Admin::AdminController

before_filter :ensure_logged_in, :ensure_staff, :fetch_embedding

@cubic-dev-ai cubic-dev-ai Bot Feb 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: :ensure_logged_in and :ensure_staff are already declared in the parent Admin::AdminController and will be inherited. Only :fetch_embedding needs to be added here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/controllers/admin/embedding_controller.rb, line 3:

<comment>`:ensure_logged_in` and `:ensure_staff` are already declared in the parent `Admin::AdminController` and will be inherited. Only `:fetch_embedding` needs to be added here.</comment>

<file context>
@@ -0,0 +1,21 @@
+class Admin::EmbeddingController < Admin::AdminController
+
+  before_filter :ensure_logged_in, :ensure_staff, :fetch_embedding
+
+  def show
</file context>
Fix with Cubic

@@ -0,0 +1,33 @@
class CreateEmbeddableHosts < ActiveRecord::Migration
def change
create_table :embeddable_hosts, force: true do |t|

@cubic-dev-ai cubic-dev-ai Bot Feb 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: force: true will silently drop and recreate the embeddable_hosts table if it already exists, causing data loss. This is inappropriate in a migration; remove force: true so the migration fails cleanly if the table already exists.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At db/migrate/20150818190757_create_embeddable_hosts.rb, line 3:

<comment>`force: true` will silently drop and recreate the `embeddable_hosts` table if it already exists, causing data loss. This is inappropriate in a migration; remove `force: true` so the migration fails cleanly if the table already exists.</comment>

<file context>
@@ -0,0 +1,33 @@
+class CreateEmbeddableHosts < ActiveRecord::Migration
+  def change
+    create_table :embeddable_hosts, force: true do |t|
+      t.string :host, null: false
+      t.integer :category_id, null: false
</file context>
Fix with Cubic

SiteSetting.embeddable_hosts = "#{host}\nhttp://discourse.org\nhttps://example.com/1234"
Fabricate(:embeddable_host)
Fabricate(:embeddable_host, host: 'http://discourse.org')
Fabricate(:embeddable_host, host: 'https://example.com/1234')

@cubic-dev-ai cubic-dev-ai Bot Feb 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Passing full URLs to host: relies on implicit before_validation stripping. For 'https://example.com/1234', the path /1234 is silently dropped, so the host stored is just example.com. This means the downstream test "works with a host with a path" doesn't actually verify path-based host matching—it's just matching on the bare domain. Consider passing clean hostnames directly (e.g., host: 'example.com') to make the test setup explicit, or if path-based matching is intended, the model and test need to be updated to support it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At spec/controllers/embed_controller_spec.rb, line 70:

<comment>Passing full URLs to `host:` relies on implicit `before_validation` stripping. For `'https://example.com/1234'`, the path `/1234` is silently dropped, so the host stored is just `example.com`. This means the downstream test "works with a host with a path" doesn't actually verify path-based host matching—it's just matching on the bare domain. Consider passing clean hostnames directly (e.g., `host: 'example.com'`) to make the test setup explicit, or if path-based matching is intended, the model and test need to be updated to support it.</comment>

<file context>
@@ -68,7 +65,9 @@
-      SiteSetting.embeddable_hosts = "#{host}\nhttp://discourse.org\nhttps://example.com/1234"
+      Fabricate(:embeddable_host)
+      Fabricate(:embeddable_host, host: 'http://discourse.org')
+      Fabricate(:embeddable_host, host: 'https://example.com/1234')
     end
 
</file context>
Fix with Cubic

Comment thread spec/models/topic_spec.rb
SiteSetting.embed_truncate = false
expect(topic.expandable_first_post?).to eq(false)
end
describe 'with an emeddable host' do

@cubic-dev-ai cubic-dev-ai Bot Feb 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Typo: 'with an emeddable host' should be 'with an embeddable host'.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At spec/models/topic_spec.rb, line 1407:

<comment>Typo: `'with an emeddable host'` should be `'with an embeddable host'`.</comment>

<file context>
@@ -1397,32 +1397,34 @@ def build_topic_with_title(title)
-      SiteSetting.embed_truncate = false
-      expect(topic.expandable_first_post?).to eq(false)
-    end
+    describe 'with an emeddable host' do
+      before do
+        Fabricate(:embeddable_host)
</file context>
Suggested change
describe 'with an emeddable host' do
describe 'with an embeddable host' do
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants