Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/assets/javascripts/admin/adapters/embedding.js.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import RestAdapter from 'discourse/adapters/rest';

export default RestAdapter.extend({
pathFor() {
return "/admin/customize/embedding";
}
});
63 changes: 63 additions & 0 deletions app/assets/javascripts/admin/components/embeddable-host.js.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { bufferedProperty } from 'discourse/mixins/buffered-content';
import computed from 'ember-addons/ember-computed-decorators';
import { on, observes } from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';

export default Ember.Component.extend(bufferedProperty('host'), {
editToggled: false,
tagName: 'tr',
categoryId: null,

editing: Ember.computed.or('host.isNew', 'editToggled'),

@on('didInsertElement')
@observes('editing')
_focusOnInput() {
Ember.run.schedule('afterRender', () => { this.$('.host-name').focus(); });
},

@computed('buffered.host', 'host.isSaving')
cantSave(host, isSaving) {
return isSaving || Ember.isEmpty(host);
},

actions: {
edit() {
this.set('categoryId', this.get('host.category.id'));
this.set('editToggled', true);
},

save() {
if (this.get('cantSave')) { return; }

const props = this.get('buffered').getProperties('host');
props.category_id = this.get('categoryId');

const host = this.get('host');
host.save(props).then(() => {
host.set('category', Discourse.Category.findById(this.get('categoryId')));
this.set('editToggled', false);
}).catch(popupAjaxError);
},

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

cancel() {
const host = this.get('host');
if (host.get('isNew')) {
this.sendAction('deleteHost', host);
} else {
this.rollbackBuffer();
this.set('editToggled', false);
}
}
}
});
Comment on lines +1 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Component logic looks solid; optionally guard focus on non-editing

The buffered editing, cantSave logic, save/delete/cancel flows, and category handling all look consistent with the template and backend API.

Minor optional tweak: _focusOnInput currently runs on every editing change, including when it becomes false. You can avoid a no‑op focus call by short‑circuiting:

  @on('didInsertElement')
  @observes('editing')
  _focusOnInput() {
    if (!this.get('editing')) { return; }
    Ember.run.schedule('afterRender', () => {
      this.$('.host-name').focus();
    });
  },

🏁 Script executed:

#!/bin/bash
# Optional: look for similar buffered-edit patterns to keep behavior consistent.
rg -n "bufferedProperty\\(" app/assets/javascripts

Length of output: 494



---

🏁 Script executed:

```shell
#!/bin/bash
# Check admin-user-field-item for similar focus/editing patterns
cat app/assets/javascripts/admin/components/admin-user-field-item.js.es6

echo "---"

# Search for other @observes methods to check for guard patterns
rg -n "@observes" app/assets/javascripts/admin/components --type=es6 -A 5

Length of output: 2911



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for @observes methods without type filter
rg -n "@observes" app/assets/javascripts/admin/components -A 3

echo "---"

# Also search for .observes pattern (older Ember syntax)
rg -n "\.observes\(" app/assets/javascripts/admin/components -A 2 | head -50

Length of output: 1877


Align focus guard with established pattern in similar components

The buffered editing, cantSave logic, save/delete/cancel flows, and category handling all look solid. However, the _focusOnInput method should include the editing guard shown in admin-user-field-item.js.es6, which uses the same bufferedProperty pattern.

Currently, _focusOnInput runs on every editing change, including when it becomes false, causing unnecessary DOM operations. The established pattern in similar components is:

_focusOnInput: function() {
  if (this.get('editing')) {
    Ember.run.schedule('afterRender', () => {
      this.$('.host-name').focus();
    });
  }
}.observes('editing').on('didInsertElement'),
🤖 Prompt for AI Agents
app/assets/javascripts/admin/components/embeddable-host.js.es6 lines 1-63:
_focusOnInput runs on every change of editing (including when editing becomes
false) and causes unnecessary DOM operations; update the method to guard on
this.get('editing') before scheduling the focus so it only focuses when editing
is true (keep the didInsertElement and observes('editing') hooks intact and
place the if-check wrapping the Ember.run.schedule call).

18 changes: 18 additions & 0 deletions app/assets/javascripts/admin/controllers/admin-embedding.js.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default Ember.Controller.extend({
embedding: null,

actions: {
saveChanges() {
this.get('embedding').update({});
},

addHost() {
const host = this.store.createRecord('embeddable-host');
this.get('embedding.embeddable_hosts').pushObject(host);
},

deleteHost(host) {
this.get('embedding.embeddable_hosts').removeObject(host);
}
Comment on lines +14 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify deletion handling for persisted hosts.

The deleteHost action removes the host from the array but doesn't explicitly mark it for deletion or call destroyRecord. Ensure that when saveChanges is called, the backend correctly identifies and deletes removed hosts from the embeddable_hosts collection.


🏁 Script executed:

#!/bin/bash
# Description: Check how the backend handles embeddable_hosts updates

# Find the embedding controller update action
rg -A 30 "def update" app/controllers/admin/embedding_controller.rb

# Check embedding serializer for how it handles has_many
rg -A 10 "embeddable_hosts" app/serializers/embedding_serializer.rb

Length of output: 405


🏁 Script executed:

#!/bin/bash
# Get the full embedding controller to see complete update method and params handling
cat app/controllers/admin/embedding_controller.rb

# Check for strong parameters and param handling
rg -n "params\|permit\|require" app/controllers/admin/embedding_controller.rb

# Look for any Rails concerns or mixins that might handle updates
rg -n "include|extend" app/controllers/admin/embedding_controller.rb

Length of output: 533


🏁 Script executed:

#!/bin/bash
# Check how the EmbeddableHost model handles associations and deletion
cat app/models/embeddable_host.rb

# Look for embedding associations
rg -B5 -A5 "embeddable_host" app/models/embedding.rb

Length of output: 625


🏁 Script executed:

#!/bin/bash
# Check if there's an embeddings resource route that might define update behavior
rg -A2 "embeddable.*host" config/routes.rb

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Find the saveChanges method in the JavaScript file to see what API calls are made
rg -B5 -A10 "saveChanges" app/assets/javascripts/admin/controllers/admin-embedding.js.es6

# Look for any controller action that handles embeddable_hosts updates
cat app/controllers/admin/embeddable_hosts_controller.rb 2>/dev/null || echo "Controller not found"

# Check if there's a separate serializer or method handling host changes
rg -n "embeddable_hosts" app/controllers/admin/*.rb

Length of output: 1306


🏁 Script executed:

#!/bin/bash
# Look for any API structure or endpoint handling in the embedding controller more broadly
rg -B3 -A15 "class Admin::EmbeddingController" app/controllers/admin/embedding_controller.rb

# Check if there's a concern or service handling the deletion logic
fd -e rb app/controllers/concerns/ -o app/services/ | xargs rg -l "embeddable" 2>/dev/null || true

Length of output: 1630


Deletion does not persist to the database. The saveChanges call cannot delete hosts.

The deleteHost action removes the host from the client-side array, but saveChanges calls embedding.update({}) with an empty object. The backend update action ignores parameters and simply re-fetches embeddable_hosts from the database without processing any changes. Deleted hosts remain in the database and will reappear after a page refresh.

To persist deletions, either:

  • Call destroy on each removed host and send DELETE requests to the embeddable_hosts endpoint, or
  • Modify the embedding controller's update action to process the embeddable_hosts IDs from parameters and delete removed records.

}
});
9 changes: 9 additions & 0 deletions app/assets/javascripts/admin/routes/admin-embedding.js.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default Ember.Route.extend({
model() {
return this.store.find('embedding');
},

setupController(controller, model) {
controller.set('embedding', model);
}
});
1 change: 1 addition & 0 deletions app/assets/javascripts/admin/routes/admin-route-map.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default {
this.resource('adminUserFields', { path: '/user_fields' });
this.resource('adminEmojis', { path: '/emojis' });
this.resource('adminPermalinks', { path: '/permalinks' });
this.resource('adminEmbedding', { path: '/embedding' });
});
this.route('api');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{{#if editing}}
<td>
{{input value=buffered.host placeholder="example.com" enter="save" class="host-name"}}
</td>
<td>
{{category-chooser value=categoryId}}
</td>
<td>
{{d-button icon="check" action="save" class="btn-primary" disabled=cantSave}}
{{d-button icon="times" action="cancel" class="btn-danger" disabled=host.isSaving}}
</td>
{{else}}
<td>{{host.host}}</td>
<td>{{category-badge host.category}}</td>
<td>
{{d-button icon="pencil" action="edit"}}
{{d-button icon="trash-o" action="delete" class='btn-danger'}}
</td>
{{/if}}
1 change: 1 addition & 0 deletions app/assets/javascripts/admin/templates/customize.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
{{nav-item route='adminEmbedding' label='admin.embedding.title'}}
{{/admin-nav}}

<div class="admin-container">
Expand Down
15 changes: 15 additions & 0 deletions app/assets/javascripts/admin/templates/embedding.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{{#if embedding.embeddable_hosts}}
<table>
<tr>
<th style='width: 50%'>{{i18n "admin.embedding.host"}}</th>
<th style='width: 30%'>{{i18n "admin.embedding.category"}}</th>
<th style='width: 20%'>&nbsp;</th>
</tr>
{{#each embedding.embeddable_hosts as |host|}}
{{embeddable-host host=host deleteHost="deleteHost"}}
{{/each}}
</table>
{{/if}}

{{d-button label="admin.embedding.add_host" action="addHost" icon="plus" class="btn-primary"}}

4 changes: 2 additions & 2 deletions app/assets/javascripts/discourse/adapters/rest.js.es6
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const ADMIN_MODELS = ['plugin', 'site-customization'];
const ADMIN_MODELS = ['plugin', 'site-customization', 'embeddable-host'];

export function Result(payload, responseJson) {
this.payload = payload;
Expand All @@ -19,7 +19,7 @@ function rethrow(error) {
export default Ember.Object.extend({

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

Expand Down
18 changes: 14 additions & 4 deletions app/assets/javascripts/discourse/models/store.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,24 @@ export default Ember.Object.extend({
_hydrateEmbedded(type, obj, root) {
const self = this;
Object.keys(obj).forEach(function(k) {
const m = /(.+)\_id$/.exec(k);
const m = /(.+)\_id(s?)$/.exec(k);
if (m) {
const subType = m[1];
const hydrated = self._lookupSubType(subType, type, obj[k], root);
if (hydrated) {
obj[subType] = hydrated;

if (m[2]) {
const hydrated = obj[k].map(function(id) {
return self._lookupSubType(subType, type, id, root);
});
obj[self.pluralize(subType)] = hydrated || [];
delete obj[k];
} else {
const hydrated = self._lookupSubType(subType, type, obj[k], root);
if (hydrated) {
obj[subType] = hydrated;
delete obj[k];
}
}

}
});
},
Expand Down
34 changes: 34 additions & 0 deletions app/controllers/admin/embeddable_hosts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class Admin::EmbeddableHostsController < Admin::AdminController

before_filter :ensure_logged_in, :ensure_staff

def create
save_host(EmbeddableHost.new)
end

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

def destroy
host = EmbeddableHost.where(id: params[:id]).first
host.destroy
render json: success_json
end

protected

def save_host(host)
host.host = params[:embeddable_host][:host]
host.category_id = params[:embeddable_host][:category_id]
host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank?

if host.save
render_serialized(host, EmbeddableHostSerializer, root: 'embeddable_host', rest_serializer: true)
else
render_json_error(host)
end
end

end
Comment on lines +1 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Handle missing embeddable hosts in update/destroy

Both update and destroy assume the record exists:

  • EmbeddableHost.where(id: params[:id]).first can return nil.
  • save_host(host) and host.destroy will then raise a NoMethodError.

Safer pattern:

  def update
-    host = EmbeddableHost.where(id: params[:id]).first
-    save_host(host)
+    host = EmbeddableHost.find_by(id: params[:id])
+    return render_not_found unless host
+    save_host(host)
  end

  def destroy
-    host = EmbeddableHost.where(id: params[:id]).first
-    host.destroy
-    render json: success_json
+    host = EmbeddableHost.find_by(id: params[:id])
+    return render_not_found unless host
+    host.destroy
+    render json: success_json
  end

This avoids 500s on bad IDs and returns a clean 404 instead.


🏁 Script executed:

#!/bin/bash
# Check for existing patterns of render_not_found usage in admin controllers.
rg -n "render_not_found" app/controllers/admin

Length of output: 0


I need to verify whether the proposed pattern is available in this codebase. Let me search more comprehensively.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check if render_not_found exists anywhere in the codebase
rg -n "render_not_found" --type rb

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Check what error handling patterns ARE used in admin controllers
rg -n -A2 "\.where\(id:" app/controllers/admin --type rb | head -40

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Check the Admin::AdminController parent class for available methods
find app/controllers -name "*admin_controller.rb" | head -5

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Search for render_not_found anywhere in codebase
rg -n "render_not_found"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check what error handling patterns exist in admin controllers
rg -n -A2 "\.where\(id:" app/controllers/admin | head -50

Length of output: 1658


🏁 Script executed:

#!/bin/bash
# Inspect Admin::AdminController for available methods
cat app/controllers/admin/admin_controller.rb

Length of output: 166


🏁 Script executed:

#!/bin/bash
# Check what methods ARE used in codebase to handle missing/not found cases
rg -n "render.*json.*error|render.*error|head" app/controllers/admin | head -20

Length of output: 2295


🏁 Script executed:

#!/bin/bash
# Look for how other update/destroy methods handle nil cases
rg -n -B2 "destroy if.*present" app/controllers/admin

Length of output: 261


Handle missing embeddable hosts in update/destroy

Both update and destroy assume the record exists:

  • EmbeddableHost.where(id: params[:id]).first can return nil.
  • Calling save_host(host) or host.destroy on nil will raise NoMethodError.

The codebase pattern (see user_fields_controller.rb:44) is to check presence before operating:

  def update
    host = EmbeddableHost.where(id: params[:id]).first
+   return render_json_error(I18n.t('not_found')) unless host
    save_host(host)
  end

  def destroy
    host = EmbeddableHost.where(id: params[:id]).first
+   return render_json_error(I18n.t('not_found')) unless host
    host.destroy
    render json: success_json
  end

This prevents 500 errors on missing IDs and returns proper error feedback.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class Admin::EmbeddableHostsController < Admin::AdminController
before_filter :ensure_logged_in, :ensure_staff
def create
save_host(EmbeddableHost.new)
end
def update
host = EmbeddableHost.where(id: params[:id]).first
save_host(host)
end
def destroy
host = EmbeddableHost.where(id: params[:id]).first
host.destroy
render json: success_json
end
protected
def save_host(host)
host.host = params[:embeddable_host][:host]
host.category_id = params[:embeddable_host][:category_id]
host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank?
if host.save
render_serialized(host, EmbeddableHostSerializer, root: 'embeddable_host', rest_serializer: true)
else
render_json_error(host)
end
end
end
class Admin::EmbeddableHostsController < Admin::AdminController
before_filter :ensure_logged_in, :ensure_staff
def create
save_host(EmbeddableHost.new)
end
def update
host = EmbeddableHost.where(id: params[:id]).first
return render_json_error(I18n.t('not_found')) unless host
save_host(host)
end
def destroy
host = EmbeddableHost.where(id: params[:id]).first
return render_json_error(I18n.t('not_found')) unless host
host.destroy
render json: success_json
end
protected
def save_host(host)
host.host = params[:embeddable_host][:host]
host.category_id = params[:embeddable_host][:category_id]
host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank?
if host.save
render_serialized(host, EmbeddableHostSerializer, root: 'embeddable_host', rest_serializer: true)
else
render_json_error(host)
end
end
end
🤖 Prompt for AI Agents
In app/controllers/admin/embeddable_hosts_controller.rb lines 1-34, both update
and destroy call methods on a host that may be nil; change the actions to fetch
the host, return a proper JSON error/404 when not found (follow the pattern in
user_fields_controller.rb:44), and only call save_host(host) or host.destroy
when host is present; ensure the response uses the same render_json_error or
not_found helper used elsewhere to avoid NoMethodError and return a graceful
error to the client.

21 changes: 21 additions & 0 deletions app/controllers/admin/embedding_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class Admin::EmbeddingController < Admin::AdminController

before_filter :ensure_logged_in, :ensure_staff, :fetch_embedding

def show
render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true)
end

def update
render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true)
end

protected

def fetch_embedding
@embedding = OpenStruct.new({
id: 'default',
embeddable_hosts: EmbeddableHost.all.order(:host)
})
end
end
3 changes: 1 addition & 2 deletions app/controllers/embed_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ def count
def ensure_embeddable

if !(Rails.env.development? && current_user.try(:admin?))
raise Discourse::InvalidAccess.new('embeddable hosts not set') if SiteSetting.embeddable_hosts.blank?
raise Discourse::InvalidAccess.new('invalid referer host') unless SiteSetting.allows_embeddable_host?(request.referer)
raise Discourse::InvalidAccess.new('invalid referer host') unless EmbeddableHost.host_allowed?(request.referer)
end

response.headers['X-Frame-Options'] = "ALLOWALL"
Expand Down
24 changes: 24 additions & 0 deletions app/models/embeddable_host.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class EmbeddableHost < ActiveRecord::Base
validates_format_of :host, :with => /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\Z/i
belongs_to :category

before_validation do
self.host.sub!(/^https?:\/\//, '')
self.host.sub!(/\/.*$/, '')
end

def self.record_for_host(host)
uri = URI(host) rescue nil
return false unless uri.present?

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

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

def self.host_allowed?(host)
record_for_host(host).present?
end

end
Comment on lines +1 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Guard against nil host and tighten host lookup/normalization

Two correctness issues and one style concern here:

  • before_validation calls self.host.sub! unconditionally; if host is ever nil (e.g., building an empty record in a console or via a malformed request), this will raise before validations run.
  • record_for_host aims for case‑insensitive matching (lower(host) = ?) but doesn’t normalize the parameter or the stored value, so behavior depends on how hosts were originally cased.
  • The URI(host) rescue nil modifier is what RuboCop is flagging.

A small refactor fixes all three:

-  before_validation do
-    self.host.sub!(/^https?:\/\//, '')
-    self.host.sub!(/\/.*$/, '')
-  end
+  before_validation do
+    return if self.host.blank?
+
+    normalized = self.host.sub(/^https?:\/\//, '').sub(/\/.*$/, '')
+    self.host = normalized.downcase
+  end
@@
-  def self.record_for_host(host)
-    uri = URI(host) rescue nil
-    return false unless uri.present?
-
-    host = uri.host
-    return false unless host.present?
-
-    where("lower(host) = ?", host).first
-  end
+  def self.record_for_host(host)
+    uri =
+      begin
+        URI(host)
+      rescue URI::InvalidURIError, ArgumentError
+        nil
+      end
+    return false unless uri && uri.host.present?
+
+    where("lower(host) = ?", uri.host.downcase).first
+  end

This keeps the public behavior (returns a record or false), but avoids callback crashes and makes the “host_allowed?” check reliably case‑insensitive.


🏁 Script executed:

#!/bin/bash
# Quick verification: search for other direct writes to EmbeddableHost#host
rg -n "EmbeddableHost" app spec

Length of output: 2439


🏁 Script executed:

#!/bin/bash
# Inspect the full spec file to understand test expectations and current behavior
cat spec/models/embeddable_host_spec.rb

Length of output: 1162


🏁 Script executed:

#!/bin/bash
# Check the admin controller to see how EmbeddableHost is created
cat app/controllers/admin/embeddable_hosts_controller.rb

Length of output: 819


🏁 Script executed:

#!/bin/bash
# Check if there's a form or API that could pass nil host
rg -n "params\[" app/controllers/admin/embeddable_hosts_controller.rb -A 2 -B 2

Length of output: 490


Guard against nil host and tighten host lookup/normalization

Two correctness issues and one style concern here:

  • before_validation calls self.host.sub! unconditionally; if host is ever nil (e.g., from an API request with missing or empty host parameter), this will raise NoMethodError before validations run.
  • record_for_host aims for case‑insensitive matching (lower(host) = ?) but doesn't normalize the parameter or consistently normalize the stored value, creating brittle behavior.
  • The URI(host) rescue nil modifier is what RuboCop flags.

A small refactor fixes all three:

-  before_validation do
-    self.host.sub!(/^https?:\/\//, '')
-    self.host.sub!(/\/.*$/, '')
-  end
+  before_validation do
+    return if self.host.blank?
+
+    normalized = self.host.sub(/^https?:\/\//, '').sub(/\/.*$/, '')
+    self.host = normalized.downcase
+  end
@@
-  def self.record_for_host(host)
-    uri = URI(host) rescue nil
-    return false unless uri.present?
-
-    host = uri.host
-    return false unless host.present?
-
-    where("lower(host) = ?", host).first
-  end
+  def self.record_for_host(host)
+    uri =
+      begin
+        URI(host)
+      rescue URI::InvalidURIError, ArgumentError
+        nil
+      end
+    return false unless uri && uri.host.present?
+
+    where("lower(host) = ?", uri.host.downcase).first
+  end

This keeps the public behavior (returns a record or false), avoids callback crashes on nil, and makes the "host_allowed?" check reliably case‑insensitive.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class EmbeddableHost < ActiveRecord::Base
validates_format_of :host, :with => /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\Z/i
belongs_to :category
before_validation do
self.host.sub!(/^https?:\/\//, '')
self.host.sub!(/\/.*$/, '')
end
def self.record_for_host(host)
uri = URI(host) rescue nil
return false unless uri.present?
host = uri.host
return false unless host.present?
where("lower(host) = ?", host).first
end
def self.host_allowed?(host)
record_for_host(host).present?
end
end
class EmbeddableHost < ActiveRecord::Base
validates_format_of :host, :with => /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\Z/i
belongs_to :category
before_validation do
return if self.host.blank?
normalized = self.host.sub(/^https?:\/\//, '').sub(/\/.*$/, '')
self.host = normalized.downcase
end
def self.record_for_host(host)
uri =
begin
URI(host)
rescue URI::InvalidURIError, ArgumentError
nil
end
return false unless uri && uri.host.present?
where("lower(host) = ?", uri.host.downcase).first
end
def self.host_allowed?(host)
record_for_host(host).present?
end
end
🧰 Tools
🪛 RuboCop (1.81.7)

[convention] 11-11: Avoid using rescue in its modifier form.

(Style/RescueModifier)

🤖 Prompt for AI Agents
In app/models/embeddable_host.rb around lines 1-24, guard the before_validation
callback against nil by returning early or calling to_s on host before running
sub! (e.g. skip normalization when host.blank?), normalize and persist host to a
canonical lowercase/stripped form (remove scheme and path, downcase) so stored
values are consistent, update record_for_host to safely parse the input without
using the rescue modifier (use begin/URI.parse/rescue) and normalize the
incoming host the same way (strip scheme/path and downcase) before querying with
where("lower(host) = ?", normalized_host), and preserve the public API of
returning a record or false.

14 changes: 0 additions & 14 deletions app/models/site_setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,6 @@ def self.anonymous_menu_items
@anonymous_menu_items ||= Set.new Discourse.anonymous_filters.map(&:to_s)
end

def self.allows_embeddable_host?(host)
return false if embeddable_hosts.blank?
uri = URI(host) rescue nil
return false unless uri.present?

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

!!embeddable_hosts.split("\n").detect {|h| h.sub(/^https?\:\/\//, '') == host }

hosts = embeddable_hosts.split("\n").map {|h| (URI(h).host rescue nil) || h }
!!hosts.detect {|h| h == host}
end

def self.anonymous_homepage
top_menu_items.map { |item| item.name }
.select { |item| anonymous_menu_items.include?(item) }
Expand Down
2 changes: 1 addition & 1 deletion app/models/topic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,7 @@ def has_topic_embed?
end

def expandable_first_post?
SiteSetting.embeddable_hosts.present? && SiteSetting.embed_truncate? && has_topic_embed?
SiteSetting.embed_truncate? && has_topic_embed?
end

TIME_TO_FIRST_RESPONSE_SQL ||= <<-SQL
Expand Down
4 changes: 3 additions & 1 deletion app/models/topic_embed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ def self.import(user, url, title, contents)
# If there is no embed, create a topic, post and the embed.
if embed.blank?
Topic.transaction do
eh = EmbeddableHost.record_for_host(url)

creator = PostCreator.new(user,
title: title,
raw: absolutize_urls(url, contents),
skip_validations: true,
cook_method: Post.cook_methods[:raw_html],
category: SiteSetting.embed_category)
category: eh.try(:category_id))
post = creator.create
if post.present?
TopicEmbed.create!(topic_id: post.topic_id,
Expand Down
16 changes: 16 additions & 0 deletions app/serializers/embeddable_host_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class EmbeddableHostSerializer < ApplicationSerializer
attributes :id, :host, :category_id

def id
object.id
end

def host
object.host
end

def category_id
object.category_id
end
end

8 changes: 8 additions & 0 deletions app/serializers/embedding_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class EmbeddingSerializer < ApplicationSerializer
attributes :id
has_many :embeddable_hosts, serializer: EmbeddableHostSerializer, embed: :ids

def id
object.id
end
end
8 changes: 8 additions & 0 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2491,6 +2491,14 @@ en:
image: "Image"
delete_confirm: "Are you sure you want to delete the :%{name}: emoji?"

embedding:
confirm_delete: "Are you sure you want to delete that host?"
title: "Embedding"
host: "Allowed Hosts"
edit: "edit"
category: "Post to Category"
add_host: "Add Host"

permalink:
title: "Permalinks"
url: "URL"
Expand Down
2 changes: 0 additions & 2 deletions config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1164,13 +1164,11 @@ en:
autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language."
highlighted_languages: "Included syntax highlighting rules. (Warning: including too many langauges may impact performance) see: https://highlightjs.org/static/demo/ for a demo"

embeddable_hosts: "Host(s) that can embed the comments from this Discourse forum. Hostname only, do not begin with http://"
feed_polling_enabled: "EMBEDDING ONLY: Whether to embed a RSS/ATOM feed as posts."
feed_polling_url: "EMBEDDING ONLY: URL of RSS/ATOM feed to embed."
embed_by_username: "Discourse username of the user who creates the embedded topics."
embed_username_key_from_feed: "Key to pull discourse username from feed."
embed_truncate: "Truncate the embedded posts."
embed_category: "Category of embedded topics."
embed_post_limit: "Maximum number of posts to embed."
embed_whitelist_selector: "CSS selector for elements that are allowed in embeds."
embed_blacklist_selector: "CSS selector for elements that are removed from embeds."
Expand Down
Loading