-
Notifications
You must be signed in to change notification settings - Fork 0
Automated Test: embed-url-handling-post #327
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| /* global discourseUrl */ | ||
| /* global discourseEmbedUrl */ | ||
| (function() { | ||
|
|
||
| var comments = document.getElementById('discourse-comments'), | ||
| iframe = document.createElement('iframe'); | ||
| iframe.src = discourseUrl + "embed/best?embed_url=" + encodeURIComponent(discourseEmbedUrl); | ||
| iframe.id = 'discourse-embed-frame'; | ||
| iframe.width = "100%"; | ||
| iframe.frameBorder = "0"; | ||
| iframe.scrolling = "no"; | ||
| comments.appendChild(iframe); | ||
|
|
||
|
|
||
| function postMessageReceived(e) { | ||
| if (!e) { return; } | ||
| if (discourseUrl.indexOf(e.origin) === -1) { return; } | ||
|
|
||
| if (e.data) { | ||
| if (e.data.type === 'discourse-resize' && e.data.height) { | ||
| iframe.height = e.data.height + "px"; | ||
| } | ||
| } | ||
| } | ||
| window.addEventListener('message', postMessageReceived, false); | ||
|
|
||
| })(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| //= require ./vendor/normalize | ||
| //= require ./common/foundation/base | ||
|
|
||
| article.post { | ||
| border-bottom: 1px solid #ddd; | ||
|
|
||
| .post-date { | ||
| float: right; | ||
| color: #aaa; | ||
| font-size: 12px; | ||
| margin: 4px 4px 0 0; | ||
| } | ||
|
|
||
| .author { | ||
| padding: 20px 0; | ||
| width: 92px; | ||
| float: left; | ||
|
|
||
| text-align: center; | ||
|
|
||
| h3 { | ||
| text-align: center; | ||
| color: #4a6b82; | ||
| font-size: 13px; | ||
| margin: 0; | ||
| } | ||
| } | ||
|
|
||
| .cooked { | ||
| padding: 20px 0; | ||
| margin-left: 92px; | ||
|
|
||
| p { | ||
| margin: 0 0 1em 0; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| header { | ||
| padding: 10px 10px 20px 10px; | ||
|
|
||
| font-size: 18px; | ||
|
|
||
| border-bottom: 1px solid #ddd; | ||
| } | ||
|
|
||
| footer { | ||
| font-size: 18px; | ||
|
|
||
| .logo { | ||
| margin-right: 10px; | ||
| margin-top: 10px; | ||
| } | ||
|
|
||
| a[href].button { | ||
| margin: 10px 0 0 10px; | ||
| } | ||
| } | ||
|
|
||
| .logo { | ||
| float: right; | ||
| max-height: 30px; | ||
| } | ||
|
|
||
| a[href].button { | ||
| background-color: #eee; | ||
| padding: 5px; | ||
| display: inline-block; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| class EmbedController < ApplicationController | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| skip_before_filter :check_xhr | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| skip_before_filter :preload_json | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| before_filter :ensure_embeddable | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| layout 'embed' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def best | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| embed_url = params.require(:embed_url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| topic_id = TopicEmbed.topic_id_for_embed(embed_url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if topic_id | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| @topic_view = TopicView.new(topic_id, current_user, {best: 5}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Jobs.enqueue(:retrieve_topic, user_id: current_user.try(:id), embed_url: embed_url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| render 'loading' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| discourse_expires_in 1.minute | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Proposed fix def best
embed_url = params.require(:embed_url)
topic_id = TopicEmbed.topic_id_for_embed(embed_url)
if topic_id
`@topic_view` = TopicView.new(topic_id, current_user, {best: 5})
+ discourse_expires_in 1.minute
else
Jobs.enqueue(:retrieve_topic, user_id: current_user.try(:id), embed_url: embed_url)
render 'loading'
end
-
- discourse_expires_in 1.minute
end📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| private | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def ensure_embeddable | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise Discourse::InvalidAccess.new('embeddable host not set') if SiteSetting.embeddable_host.blank? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise Discourse::InvalidAccess.new('invalid referer host') if URI(request.referer || '').host != SiteSetting.embeddable_host | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.headers['X-Frame-Options'] = "ALLOWALL" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rescue URI::InvalidURIError | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise Discourse::InvalidAccess.new('invalid referer host') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Two observations on
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| require_dependency 'email/sender' | ||
| require_dependency 'topic_retriever' | ||
|
|
||
| module Jobs | ||
|
|
||
| # Asynchronously retrieve a topic from an embedded site | ||
| class RetrieveTopic < Jobs::Base | ||
|
|
||
| def execute(args) | ||
| raise Discourse::InvalidParameters.new(:embed_url) unless args[:embed_url].present? | ||
|
|
||
| user = nil | ||
| if args[:user_id] | ||
| user = User.where(id: args[:user_id]).first | ||
| end | ||
|
|
||
| TopicRetriever.new(args[:embed_url], no_throttle: user.try(:staff?)).retrieve | ||
| end | ||
|
|
||
| end | ||
|
|
||
| end | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||||||||
| # | ||||||||||||
| # Creates and Updates Topics based on an RSS or ATOM feed. | ||||||||||||
| # | ||||||||||||
| require 'digest/sha1' | ||||||||||||
| require_dependency 'post_creator' | ||||||||||||
| require_dependency 'post_revisor' | ||||||||||||
| require 'open-uri' | ||||||||||||
|
|
||||||||||||
| module Jobs | ||||||||||||
| class PollFeed < Jobs::Scheduled | ||||||||||||
| recurrence { hourly } | ||||||||||||
| sidekiq_options retry: false | ||||||||||||
|
|
||||||||||||
| def execute(args) | ||||||||||||
| poll_feed if SiteSetting.feed_polling_enabled? && | ||||||||||||
| SiteSetting.feed_polling_url.present? && | ||||||||||||
| SiteSetting.embed_by_username.present? | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| def feed_key | ||||||||||||
| @feed_key ||= "feed-modified:#{Digest::SHA1.hexdigest(SiteSetting.feed_polling_url)}" | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| def poll_feed | ||||||||||||
| user = User.where(username_lower: SiteSetting.embed_by_username.downcase).first | ||||||||||||
| return if user.blank? | ||||||||||||
|
|
||||||||||||
| require 'simple-rss' | ||||||||||||
| rss = SimpleRSS.parse open(SiteSetting.feed_polling_url) | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Brakeman's flag is valid here. In Ruby versions before 3.0, Proposed fix- rss = SimpleRSS.parse open(SiteSetting.feed_polling_url)
+ rss = SimpleRSS.parse URI.open(SiteSetting.feed_polling_url)📝 Committable suggestion
Suggested change
🧰 Tools🪛 Brakeman (8.0.1)[medium] 29-29: Model attribute used in file name (File Access) 🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| rss.items.each do |i| | ||||||||||||
| url = i.link | ||||||||||||
| url = i.id if url.blank? || url !~ /^https?\:\/\// | ||||||||||||
|
|
||||||||||||
| content = CGI.unescapeHTML(i.content.scrub) | ||||||||||||
| TopicEmbed.import(user, url, i.title, content) | ||||||||||||
|
Comment on lines
+35
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not all RSS/Atom items have a Proposed fix- content = CGI.unescapeHTML(i.content.scrub)
+ raw_content = i.content || i.description || i.summary || ''
+ content = CGI.unescapeHTML(raw_content.scrub)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| end | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| end | ||||||||||||
| end | ||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,82 @@ | ||||||||
| require_dependency 'nokogiri' | ||||||||
|
|
||||||||
| class TopicEmbed < ActiveRecord::Base | ||||||||
| belongs_to :topic | ||||||||
| belongs_to :post | ||||||||
| validates_presence_of :embed_url | ||||||||
| validates_presence_of :content_sha1 | ||||||||
|
|
||||||||
| # Import an article from a source (RSS/Atom/Other) | ||||||||
| def self.import(user, url, title, contents) | ||||||||
| return unless url =~ /^https?\:\/\// | ||||||||
|
|
||||||||
| contents << "\n<hr>\n<small>#{I18n.t('embed.imported_from', link: "<a href='#{url}'>#{url}</a>")}</small>\n" | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mutates the caller's Two issues on this line:
Proposed fix- contents << "\n<hr>\n<small>#{I18n.t('embed.imported_from', link: "<a href='#{url}'>#{url}</a>")}</small>\n"
+ escaped_url = ERB::Util.html_escape(url)
+ contents = contents + "\n<hr>\n<small>#{I18n.t('embed.imported_from', link: "<a href='#{escaped_url}'>#{escaped_url}</a>")}</small>\n"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| embed = TopicEmbed.where(embed_url: url).first | ||||||||
| content_sha1 = Digest::SHA1.hexdigest(contents) | ||||||||
| post = nil | ||||||||
|
|
||||||||
| # If there is no embed, create a topic, post and the embed. | ||||||||
| if embed.blank? | ||||||||
| Topic.transaction do | ||||||||
| creator = PostCreator.new(user, title: title, raw: absolutize_urls(url, contents), skip_validations: true, cook_method: Post.cook_methods[:raw_html]) | ||||||||
| post = creator.create | ||||||||
| if post.present? | ||||||||
| TopicEmbed.create!(topic_id: post.topic_id, | ||||||||
| embed_url: url, | ||||||||
| content_sha1: content_sha1, | ||||||||
| post_id: post.id) | ||||||||
| end | ||||||||
| end | ||||||||
| else | ||||||||
| post = embed.post | ||||||||
| # Update the topic if it changed | ||||||||
| if content_sha1 != embed.content_sha1 | ||||||||
| revisor = PostRevisor.new(post) | ||||||||
| revisor.revise!(user, absolutize_urls(url, contents), skip_validations: true, bypass_rate_limiter: true) | ||||||||
| embed.update_column(:content_sha1, content_sha1) | ||||||||
| end | ||||||||
| end | ||||||||
|
|
||||||||
| post | ||||||||
| end | ||||||||
|
|
||||||||
| def self.import_remote(user, url, opts=nil) | ||||||||
| require 'ruby-readability' | ||||||||
|
|
||||||||
| opts = opts || {} | ||||||||
| doc = Readability::Document.new(open(url).read, | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Unlike the feed polling URL (admin-controlled), this Use Proposed fix- doc = Readability::Document.new(open(url).read,
+ doc = Readability::Document.new(URI.open(url).read,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
| tags: %w[div p code pre h1 h2 h3 b em i strong a img], | ||||||||
| attributes: %w[href src]) | ||||||||
|
|
||||||||
| TopicEmbed.import(user, url, opts[:title] || doc.title, doc.content) | ||||||||
| end | ||||||||
|
|
||||||||
| # Convert any relative URLs to absolute. RSS is annoying for this. | ||||||||
| def self.absolutize_urls(url, contents) | ||||||||
| uri = URI(url) | ||||||||
| prefix = "#{uri.scheme}://#{uri.host}" | ||||||||
| prefix << ":#{uri.port}" if uri.port != 80 && uri.port != 443 | ||||||||
|
|
||||||||
| fragment = Nokogiri::HTML.fragment(contents) | ||||||||
| fragment.css('a').each do |a| | ||||||||
| href = a['href'] | ||||||||
| if href.present? && href.start_with?('/') | ||||||||
| a['href'] = "#{prefix}/#{href.sub(/^\/+/, '')}" | ||||||||
| end | ||||||||
| end | ||||||||
| fragment.css('img').each do |a| | ||||||||
| src = a['src'] | ||||||||
| if src.present? && src.start_with?('/') | ||||||||
| a['src'] = "#{prefix}/#{src.sub(/^\/+/, '')}" | ||||||||
| end | ||||||||
| end | ||||||||
|
|
||||||||
| fragment.to_html | ||||||||
| end | ||||||||
|
|
||||||||
| def self.topic_id_for_embed(embed_url) | ||||||||
| TopicEmbed.where(embed_url: embed_url).pluck(:topic_id).first | ||||||||
| end | ||||||||
|
|
||||||||
| end | ||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,30 @@ | ||||||||||||||||||
| <header> | ||||||||||||||||||
| <%- if @topic_view.posts.present? %> | ||||||||||||||||||
| <%= link_to(I18n.t('embed.title'), @topic_view.topic.url, class: 'button', target: '_blank') %> | ||||||||||||||||||
| <%- else %> | ||||||||||||||||||
| <%= link_to(I18n.t('embed.start_discussion'), @topic_view.topic.url, class: 'button', target: '_blank') %> | ||||||||||||||||||
| <%- end if %> | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Syntax error:
🐛 Proposed fix- <%- end if %>
+ <%- end %>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| <%= link_to(image_tag(SiteSetting.logo_url, class: 'logo'), Discourse.base_url) %> | ||||||||||||||||||
| </header> | ||||||||||||||||||
|
|
||||||||||||||||||
| <%- if @topic_view.posts.present? %> | ||||||||||||||||||
| <%- @topic_view.posts.each do |post| %> | ||||||||||||||||||
| <article class='post'> | ||||||||||||||||||
| <%= link_to post.created_at.strftime("%e %b %Y"), post.url, class: 'post-date', target: "_blank" %> | ||||||||||||||||||
| <div class='author'> | ||||||||||||||||||
| <img src='<%= post.user.small_avatar_url %>'> | ||||||||||||||||||
| <h3><%= post.user.username %></h3> | ||||||||||||||||||
| </div> | ||||||||||||||||||
|
Comment on lines
+15
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unescaped URL interpolation in If 🛡️ Proposed fix- <img src='<%= post.user.small_avatar_url %>'>
+ <%= image_tag(post.user.small_avatar_url) %>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| <div class='cooked'><%= raw post.cooked %></div> | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: rg -n 'raw_html' --type ruby -C 3Repository: admin-coderabbit/coderabbit-discourse-mirror Length of output: 1990 🏁 Script executed: rg -n 'absolutize_urls' --type ruby -B 2 -A 5Repository: admin-coderabbit/coderabbit-discourse-mirror Length of output: 1777 🏁 Script executed: rg -n 'TopicEmbed\.import' --type ruby -B 3 -A 10Repository: admin-coderabbit/coderabbit-discourse-mirror Length of output: 4045 🏁 Script executed: # Check for any sanitization in the TopicEmbed or embed context
rg -n 'sanitize|Sanitize' --type ruby app/models/topic_embed.rb -C 3Repository: admin-coderabbit/coderabbit-discourse-mirror Length of output: 70 🏁 Script executed: rg -n 'import_remote' --type ruby -B 2 -A 15Repository: admin-coderabbit/coderabbit-discourse-mirror Length of output: 1499 🏁 Script executed: # Check where TopicEmbed.import is called and what kind of content is passed
rg -n 'TopicEmbed\.import' --type ruby -B 5 -A 2 | head -50Repository: admin-coderabbit/coderabbit-discourse-mirror Length of output: 3092 Raw HTML rendering in embed view bypasses Discourse's sanitization pipeline — this is a security vulnerability, not a safe pattern. The Ensure HTML sanitization via Discourse's 🤖 Prompt for AI Agents |
||||||||||||||||||
| <div style='clear: both'></div> | ||||||||||||||||||
| </article> | ||||||||||||||||||
| <%- end %> | ||||||||||||||||||
|
|
||||||||||||||||||
| <footer> | ||||||||||||||||||
| <%= link_to(I18n.t('embed.continue'), @topic_view.topic.url, class: 'button', target: '_blank') %> | ||||||||||||||||||
| <%= link_to(image_tag(SiteSetting.logo_url, class: 'logo'), Discourse.base_url) %> | ||||||||||||||||||
| </footer> | ||||||||||||||||||
|
|
||||||||||||||||||
| <% end %> | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential NPE if
#discourse-commentselement is missing.If
document.getElementById('discourse-comments')returnsnull, theappendChildcall on Line 12 will throw aTypeError. Consider adding a guard.Also, Line 7: if
discourseUrldoesn't end with a trailing/, the constructed URL will be malformed (e.g.,"https://example.comembed/best?...").🐛 Proposed fix
var comments = document.getElementById('discourse-comments'), iframe = document.createElement('iframe'); - iframe.src = discourseUrl + "embed/best?embed_url=" + encodeURIComponent(discourseEmbedUrl); + iframe.src = discourseUrl + (discourseUrl[discourseUrl.length-1] === '/' ? '' : '/') + "embed/best?embed_url=" + encodeURIComponent(discourseEmbedUrl); iframe.id = 'discourse-embed-frame'; iframe.width = "100%"; iframe.frameBorder = "0"; iframe.scrolling = "no"; - comments.appendChild(iframe); + if (comments) { comments.appendChild(iframe); }🤖 Prompt for AI Agents