Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport 'Improve the link handling' to v0.27 #10735

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 8 additions & 11 deletions decidim-core/app/controllers/decidim/links_controller.rb
Expand Up @@ -21,24 +21,21 @@ def new

def invalid_url
flash[:alert] = I18n.t("decidim.links.invalid_url")
redirect_to decidim.root_path
if request.xhr?
render "invalid_url"
else
redirect_to decidim.root_path
end
end

def parse_url
raise Decidim::InvalidUrlError if params[:external_url].blank?
raise Decidim::InvalidUrlError unless external_url

parts = external_url.match %r{\A(([a-z]+):)?//([^/]+)(/.*)?\z}
raise Decidim::InvalidUrlError unless parts

@url_parts = {
protocol: parts[1],
domain: parts[3],
path: parts[4]
}
raise Decidim::InvalidUrlError unless %w(http https).include?(external_url.scheme)
end

def external_url
@external_url ||= URI.parse(params[:external_url]).to_s
@external_url ||= URI.parse(params[:external_url])
end
end
end
17 changes: 14 additions & 3 deletions decidim-core/app/helpers/decidim/external_domain_helper.rb
Expand Up @@ -3,10 +3,21 @@
module Decidim
module ExternalDomainHelper
def highlight_domain
highlighted_domain = [
external_url.host,
(external_url.port && [80, 443].include?(external_url.port) ? "" : ":#{external_url.port}")
].join

path = [
external_url.path,
(external_url.query ? "?#{external_url.query}" : ""),
(external_url.fragment ? "##{external_url.fragment}" : "")
].join

tag.div do
content_tag(:span, "#{@url_parts[:protocol]}//") +
content_tag(:span, @url_parts[:domain], class: "alert") +
content_tag(:span, @url_parts[:path])
content_tag(:span, "#{external_url.scheme}://") +
content_tag(:span, highlighted_domain, class: "text-alert") +
content_tag(:span, path)
end
end
end
Expand Down
17 changes: 17 additions & 0 deletions decidim-core/app/views/decidim/links/_invalid_url_modal.html.erb
@@ -0,0 +1,17 @@
<%= decidim_modal id: "external-domain-warning" do %>
<div data-dialog-container>
<%= icon "external-link-line" %>
<h2 id="dialog-title-external-domain-warning" tabindex="-1" data-dialog-title><%= t("decidim.links.warning.title") %></h2>
<div>

<code class="mt-5 block break-all text-alert">
<%= flash[:alert] %>
</code>
</div>
</div>
<div data-dialog-actions>
<button class="button button__lg button__transparent-secondary" data-dialog-close="external-domain-warning">
<%= t("decidim.links.warning.cancel") %>
</button>
</div>
<% end %>
2 changes: 1 addition & 1 deletion decidim-core/app/views/decidim/links/_modal.html.erb
Expand Up @@ -13,7 +13,7 @@
</div>
</div>
<div class="row buttons">
<%= link_to t("decidim.links.warning.proceed"), params[:external_url], target: "_blank", data: { close: "" }, class: "button primary button--nomargin" %>
<%= link_to t("decidim.links.warning.proceed"), external_url.to_s, target: "_blank", data: { close: "" }, class: "button primary button--nomargin" %>
<button class="button clear" data-close>
<%= t("decidim.links.warning.cancel") %>
</button>
Expand Down
24 changes: 24 additions & 0 deletions decidim-core/app/views/decidim/links/invalid_url.js.erb
@@ -0,0 +1,24 @@
(function() {
const create = (selector) => {
const element = document.createElement("div")
element.id = selector
document.body.append(element)
return element
}

const selector = "external-domain-warning"
const selectorContainer = `${selector}-container`

// if the container does not exist in the DOM, it creates a new one, otherwise, replace the content
const externalDomainWarning = document.getElementById(selectorContainer) || create(selectorContainer)

externalDomainWarning.innerHTML = ''
externalDomainWarning.innerHTML = '<%= j(render partial: "invalid_url_modal").strip.html_safe %>'

new window.Decidim.Dialogs(`#${selector}`, {
closingSelector: `[data-dialog-close="${selector}"]`,
backdropSelector: `[data-dialog="${selector}"]`,
labelledby: `dialog-title-${selector}`,
describedby: `dialog-desc-${selector}`
}).open()
})()
2 changes: 1 addition & 1 deletion decidim-core/app/views/decidim/links/new.html.erb
Expand Up @@ -12,7 +12,7 @@
</div>
<div class="row">
<div class="columns large-12 text-center">
<%= link_to t("decidim.links.warning.proceed"), params[:external_url], class: "button expanded primary" %>
<%= link_to t("decidim.links.warning.proceed"), external_url.to_s, class: "button expanded primary" %>
</div>
</div>
</div>
Expand Down
Expand Up @@ -5,10 +5,14 @@
module Decidim
describe ExternalDomainHelper do
context "when everything is OK" do
before { helper.instance_variable_set(:@url_parts, { protocol: "https:", domain: "decidim.barcelona", path: "/processes" }) }
before do
# rubocop:disable RSpec/AnyInstance
allow_any_instance_of(ActionView::Base).to receive(:external_url).and_return(URI.parse("https://decidim.barcelona/processes"))
# rubocop:enable RSpec/AnyInstance
end

it "highlights domain" do
expect(helper.highlight_domain).to include('class="alert">decidim.barcelona')
expect(helper.highlight_domain).to include('class="text-alert">decidim.barcelona')
end
end
end
Expand Down
93 changes: 80 additions & 13 deletions decidim-core/spec/system/external_domain_warning_spec.rb
Expand Up @@ -37,11 +37,84 @@
end
end

context "when the url is malformed" do
let(:invalid_url) do
"http://#{organization.host}/link?external_url=javascript:alert(document.location.host)//%0ahttps://www.example.org"
context "when url has missing protocols" do
let(:invalid_url) { "http://#{organization.host}/link?external_url=//example.org/some/path" }

it "shows invalid url alert" do
visit invalid_url
expect(page).to have_content("Invalid URL")
end
let!(:invalid_url2) do
end

context "when url has a port" do
let(:destination) { "http://example.org:3000/some/path" }
let(:invalid_url) { "http://#{organization.host}/link?external_url=#{destination}" }

it "shows invalid url alert" do
visit invalid_url
expect(page).not_to have_content("Invalid URL")
expect(page).to have_content(destination)
expect(page).to have_link("Proceed", href: destination)
end
end

context "when url has invalid protocols" do
let(:invalid_url) { "http://#{organization.host}/link?external_url=javascript://example.org%250aalert(document.location.host)" }

it "shows invalid url alert" do
visit invalid_url
expect(page).to have_content("Invalid URL")
end
end

context "when url is double encoded" do
let(:invalid_url) { "http://#{organization.host}/link?external_url=http://example.org%250ajavascript:alert(document.location.host)" }

it "shows invalid url alert" do
visit invalid_url
expect(page).to have_content("Invalid URL")
end
end

context "when url is not HTTP" do
let(:invalid_url) { "http://#{organization.host}/link?external_url=ftp://example.org/some/path" }

it "shows invalid url alert" do
visit invalid_url
expect(page).to have_content("Invalid URL")
end
end

context "when url starts with HTTP but is not valid protocol" do
let(:invalid_url) { "http://#{organization.host}/link?external_url=httpfoobar://example.org/some/path" }

it "shows invalid url alert" do
visit invalid_url
expect(page).to have_content("Invalid URL")
end
end

context "when url starts with HTTPS but is not valid protocol" do
let(:invalid_url) { "http://#{organization.host}/link?external_url=httpsfoobar://example.org/some/path" }

it "shows invalid url alert" do
visit invalid_url
expect(page).to have_content("Invalid URL")
end
end

context "when the url is malformed using a simple scenario" do
let(:invalid_url) { "http://#{organization.host}/link?external_url=javascript:alert(document.location.host)//%0ahttps://www.example.org" }

it "shows invalid url alert when using simple scenario" do
visit invalid_url
expect(page).to have_content("Invalid URL")
expect(page).to have_current_path(decidim.root_path, ignore_query: true)
end
end

context "when the url is malformed using a complex scenario" do
let(:invalid_url) do
%W(
http://#{organization.host}/link?external_url=javascript:fetch%28%22%2Fprocesses%2Fconsequuntur%2Daperiam%2Ff%2F12%2F
proposals%2F8%2Fproposal%5Fvote%22%2C%20%7B%22headers%22%3A%7B%22x%2Dcsrf%2Dtoken%22%3Adocument%2EquerySelectorAll%28
Expand All @@ -51,24 +124,18 @@
).join
end

it "shows invalid url alert when using simple scenario" do
visit invalid_url
expect(page).to have_content("Invalid URL")
expect(page).to have_current_path(decidim.root_path, ignore_query: true)
end

it "shows invalid url alert when using complex scenario" do
visit invalid_url2
visit invalid_url
expect(page).to have_content("Invalid URL")
expect(page).to have_current_path(decidim.root_path, ignore_query: true)
end
end

context "without param" do
let(:no_param) { "http://#{organization.host}/link" }
let(:invalid_url) { "http://#{organization.host}/link" }

it "shows invalid url alert" do
visit no_param
visit invalid_url
expect(page).to have_content("Invalid URL")
expect(page).to have_current_path decidim.root_path
end
Expand Down