Commit 3df58f14 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'gh-labels' into 'master'

Allow `?`, or `&` for label names

Closes #18727 

See merge request !4724
parents be018ba8 e89a515c
...@@ -42,6 +42,7 @@ v 8.10.0 (unreleased) ...@@ -42,6 +42,7 @@ v 8.10.0 (unreleased)
- Don't garbage collect commits that have related DB records like comments - Don't garbage collect commits that have related DB records like comments
- More descriptive message for git hooks and file locks - More descriptive message for git hooks and file locks
- Handle custom Git hook result in GitLab UI - Handle custom Git hook result in GitLab UI
- Allow '?', or '&' for label names
v 8.9.5 (unreleased) v 8.9.5 (unreleased)
- Improve the request / withdraw access button. !4860 - Improve the request / withdraw access button. !4860
......
...@@ -190,7 +190,7 @@ GitLab.GfmAutoComplete = ...@@ -190,7 +190,7 @@ GitLab.GfmAutoComplete =
callbacks: callbacks:
beforeSave: (merges) -> beforeSave: (merges) ->
sanitizeLabelTitle = (title)-> sanitizeLabelTitle = (title)->
if /\w+\s+\w+/g.test(title) if /[\w\?&]+\s+[\w\?&]+/g.test(title)
"\"#{sanitize(title)}\"" "\"#{sanitize(title)}\""
else else
sanitize(title) sanitize(title)
......
...@@ -32,7 +32,7 @@ class @LabelsSelect ...@@ -32,7 +32,7 @@ class @LabelsSelect
if issueUpdateURL if issueUpdateURL
labelHTMLTemplate = _.template( labelHTMLTemplate = _.template(
'<% _.each(labels, function(label){ %> '<% _.each(labels, function(label){ %>
<a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- label.title %>"> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>">
<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">
<%- label.title %> <%- label.title %>
</span> </span>
...@@ -261,7 +261,7 @@ class @LabelsSelect ...@@ -261,7 +261,7 @@ class @LabelsSelect
$a.attr('data-label-id', label.id) $a.attr('data-label-id', label.id)
$a.addClass(selectedClass.join(' ')) $a.addClass(selectedClass.join(' '))
.html("#{colorEl} #{_.escape(label.title)}") .html("#{colorEl} #{label.title}")
# Return generated html # Return generated html
$li.html($a).prop('outerHTML') $li.html($a).prop('outerHTML')
...@@ -288,7 +288,7 @@ class @LabelsSelect ...@@ -288,7 +288,7 @@ class @LabelsSelect
fieldName: $dropdown.data('field-name') fieldName: $dropdown.data('field-name')
id: (label) -> id: (label) ->
if $dropdown.hasClass("js-filter-submit") and not label.isAny? if $dropdown.hasClass("js-filter-submit") and not label.isAny?
_.escape label.title label.title
else else
label.id label.id
......
...@@ -20,10 +20,10 @@ class Label < ActiveRecord::Base ...@@ -20,10 +20,10 @@ class Label < ActiveRecord::Base
validates :color, color: true, allow_blank: false validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? } validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow '?', '&', and ',' for label titles # Don't allow ',' for label titles
validates :title, validates :title,
presence: true, presence: true,
format: { with: /\A[^&\?,]+\z/ }, format: { with: /\A[^,]+\z/ },
uniqueness: { scope: :project_id } uniqueness: { scope: :project_id }
before_save :nullify_priority before_save :nullify_priority
...@@ -58,8 +58,8 @@ class Label < ActiveRecord::Base ...@@ -58,8 +58,8 @@ class Label < ActiveRecord::Base
(?: (?:
(?<label_id>\d+) | # Integer-based label ID, or (?<label_id>\d+) | # Integer-based label ID, or
(?<label_name> (?<label_name>
[A-Za-z0-9_-]+ | # String-based single-word label title, or [A-Za-z0-9_\-\?&]+ | # String-based single-word label title, or
"[^&\?,]+" # String-based multi-word label surrounded in quotes "[^,]+" # String-based multi-word label surrounded in quotes
) )
) )
}x }x
...@@ -114,7 +114,7 @@ class Label < ActiveRecord::Base ...@@ -114,7 +114,7 @@ class Label < ActiveRecord::Base
end end
def title=(value) def title=(value)
write_attribute(:title, Sanitize.clean(value.to_s)) if value.present? write_attribute(:title, sanitize_title(value)) if value.present?
end end
private private
...@@ -132,4 +132,8 @@ class Label < ActiveRecord::Base ...@@ -132,4 +132,8 @@ class Label < ActiveRecord::Base
def nullify_priority def nullify_priority
self.priority = nil if priority.blank? self.priority = nil if priority.blank?
end end
def sanitize_title(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s))
end
end end
- labels.each do |label| - labels.each do |label|
%span.label-row.btn-group{ role: "group", aria: { label: escape_once(label.name) }, style: "color: #{text_color_for_bg(label.color)}" } %span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" }
= link_to label_filter_path(@project, label, type: controller.controller_name), = link_to label.name, label_filter_path(@project, label, type: controller.controller_name),
class: "btn btn-transparent has-tooltip", class: "btn btn-transparent has-tooltip",
style: "background-color: #{label.color};", style: "background-color: #{label.color};",
title: escape_once(label.description), title: escape_once(label.description),
data: { container: "body" } do data: { container: "body" }
= escape_once label.name
%button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } } %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } }
= icon("times") = icon("times")
...@@ -13,13 +13,13 @@ module Banzai ...@@ -13,13 +13,13 @@ module Banzai
end end
def self.references_in(text, pattern = Label.reference_pattern) def self.references_in(text, pattern = Label.reference_pattern)
text.gsub(pattern) do |match| unescape_html_entities(text).gsub(pattern) do |match|
yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~ yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~
end end
end end
def references_in(text, pattern = Label.reference_pattern) def references_in(text, pattern = Label.reference_pattern)
text.gsub(pattern) do |match| unescape_html_entities(text).gsub(pattern) do |match|
label = find_label($~[:project], $~[:label_id], $~[:label_name]) label = find_label($~[:project], $~[:label_id], $~[:label_name])
if label if label
...@@ -66,6 +66,10 @@ module Banzai ...@@ -66,6 +66,10 @@ module Banzai
LabelsHelper.render_colored_cross_project_label(object) LabelsHelper.render_colored_cross_project_label(object)
end end
end end
def unescape_html_entities(text)
CGI.unescapeHTML(text.to_s)
end
end end
end end
end end
...@@ -104,6 +104,31 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do ...@@ -104,6 +104,31 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end end
end end
context 'String-based single-word references with special characters' do
let(:label) { create(:label, name: '?gfm&', project: project) }
let(:reference) { "#{Label.reference_prefix}#{label.name}" }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
expect(doc.text).to eq 'See ?gfm&'
end
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>\?gfm&amp;</span></a>\.\)))
end
it 'ignores invalid label names' do
act = "Label #{Label.reference_prefix}#{label.name.reverse}"
exp = "Label #{Label.reference_prefix}&amp;mfg?"
expect(reference_filter(act).to_html).to eq exp
end
end
context 'String-based multi-word references in quotes' do context 'String-based multi-word references in quotes' do
let(:label) { create(:label, name: 'gfm references', project: project) } let(:label) { create(:label, name: 'gfm references', project: project) }
let(:reference) { label.to_reference(format: :name) } let(:reference) { label.to_reference(format: :name) }
...@@ -128,6 +153,31 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do ...@@ -128,6 +153,31 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end end
end end
context 'String-based multi-word references with special characters in quotes' do
let(:label) { create(:label, name: 'gfm & references?', project: project) }
let(:reference) { label.to_reference(format: :name) }
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
expect(doc.text).to eq 'See gfm & references?'
end
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>gfm &amp; references\?</span></a>\.\)))
end
it 'ignores invalid label names' do
act = %(Label #{Label.reference_prefix}"#{label.name.reverse}")
exp = %(Label #{Label.reference_prefix}"?secnerefer &amp; mfg\")
expect(reference_filter(act).to_html).to eq exp
end
end
describe 'edge cases' do describe 'edge cases' do
it 'gracefully handles non-references matching the pattern' do it 'gracefully handles non-references matching the pattern' do
exp = act = '(format nil "~0f" 3.0) ; 3.0' exp = act = '(format nil "~0f" 3.0) ; 3.0'
......
...@@ -32,21 +32,20 @@ describe Label, models: true do ...@@ -32,21 +32,20 @@ describe Label, models: true do
it 'should validate title' do it 'should validate title' do
expect(label).not_to allow_value('G,ITLAB').for(:title) expect(label).not_to allow_value('G,ITLAB').for(:title)
expect(label).not_to allow_value('G?ITLAB').for(:title)
expect(label).not_to allow_value('G&ITLAB').for(:title)
expect(label).not_to allow_value('').for(:title) expect(label).not_to allow_value('').for(:title)
expect(label).to allow_value('GITLAB').for(:title) expect(label).to allow_value('GITLAB').for(:title)
expect(label).to allow_value('gitlab').for(:title) expect(label).to allow_value('gitlab').for(:title)
expect(label).to allow_value('G?ITLAB').for(:title)
expect(label).to allow_value('G&ITLAB').for(:title)
expect(label).to allow_value("customer's request").for(:title) expect(label).to allow_value("customer's request").for(:title)
end end
end end
describe "#title" do describe '#title' do
let(:label) { create(:label, title: "<b>test</b>") } it 'sanitizes title' do
label = described_class.new(title: '<b>foo & bar?</b>')
it "sanitizes title" do expect(label.title).to eq('foo & bar?')
expect(label.title).to eq("test")
end end
end end
......
...@@ -482,12 +482,16 @@ describe API::API, api: true do ...@@ -482,12 +482,16 @@ describe API::API, api: true do
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
end end
it 'should return 400 on invalid label names' do it 'should allow special label names' do
post api("/projects/#{project.id}/issues", user), post api("/projects/#{project.id}/issues", user),
title: 'new issue', title: 'new issue',
labels: 'label, ?' labels: 'label, label?, label&foo, ?, &'
expect(response).to have_http_status(400) expect(response.status).to eq(201)
expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end end
it 'should return 400 if title is too long' do it 'should return 400 if title is too long' do
...@@ -557,12 +561,17 @@ describe API::API, api: true do ...@@ -557,12 +561,17 @@ describe API::API, api: true do
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
it 'should return 400 on invalid label names' do it 'should allow special label names' do
put api("/projects/#{project.id}/issues/#{issue.id}", user), put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title', title: 'updated title',
labels: 'label, ?' labels: 'label, label?, label&foo, ?, &'
expect(response).to have_http_status(400)
expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end end
context 'confidential issues' do context 'confidential issues' do
...@@ -627,21 +636,18 @@ describe API::API, api: true do ...@@ -627,21 +636,18 @@ describe API::API, api: true do
expect(json_response['labels']).to include 'bar' expect(json_response['labels']).to include 'bar'
end end
it 'should return 400 on invalid label names' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label, ?'
expect(response).to have_http_status(400)
expect(json_response['message']['labels']['?']['title']).to eq(['is invalid'])
end
it 'should allow special label names' do it 'should allow special label names' do
put api("/projects/#{project.id}/issues/#{issue.id}", user), put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label:foo, label-bar,label_bar,label/bar' labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
expect(response).to have_http_status(200) expect(response.status).to eq(200)
expect(json_response['labels']).to include 'label:foo' expect(json_response['labels']).to include 'label:foo'
expect(json_response['labels']).to include 'label-bar' expect(json_response['labels']).to include 'label-bar'
expect(json_response['labels']).to include 'label_bar' expect(json_response['labels']).to include 'label_bar'
expect(json_response['labels']).to include 'label/bar' expect(json_response['labels']).to include 'label/bar'
expect(json_response['labels']).to include 'label?bar'
expect(json_response['labels']).to include 'label&bar'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end end
it 'should return 400 if title is too long' do it 'should return 400 if title is too long' do
......
...@@ -35,10 +35,10 @@ describe API::API, api: true do ...@@ -35,10 +35,10 @@ describe API::API, api: true do
it 'should return created label when only required params' do it 'should return created label when only required params' do
post api("/projects/#{project.id}/labels", user), post api("/projects/#{project.id}/labels", user),
name: 'Foo', name: 'Foo & Bar',
color: '#FFAABB' color: '#FFAABB'
expect(response).to have_http_status(201) expect(response.status).to eq(201)
expect(json_response['name']).to eq('Foo') expect(json_response['name']).to eq('Foo & Bar')
expect(json_response['color']).to eq('#FFAABB') expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to be_nil expect(json_response['description']).to be_nil
end end
...@@ -71,7 +71,7 @@ describe API::API, api: true do ...@@ -71,7 +71,7 @@ describe API::API, api: true do
it 'should return 400 for invalid name' do it 'should return 400 for invalid name' do
post api("/projects/#{project.id}/labels", user), post api("/projects/#{project.id}/labels", user),
name: '?', name: ',',
color: '#FFAABB' color: '#FFAABB'
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq(['is invalid']) expect(json_response['message']['title']).to eq(['is invalid'])
...@@ -167,7 +167,7 @@ describe API::API, api: true do ...@@ -167,7 +167,7 @@ describe API::API, api: true do
it 'should return 400 for invalid name' do it 'should return 400 for invalid name' do
put api("/projects/#{project.id}/labels", user), put api("/projects/#{project.id}/labels", user),
name: 'label1', name: 'label1',
new_name: '?', new_name: ',',
color: '#FFFFFF' color: '#FFFFFF'
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq(['is invalid']) expect(json_response['message']['title']).to eq(['is invalid'])
......
...@@ -243,17 +243,19 @@ describe API::API, api: true do ...@@ -243,17 +243,19 @@ describe API::API, api: true do
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
end end
it 'should return 400 on invalid label names' do it 'should allow special label names' do
post api("/projects/#{project.id}/merge_requests", user), post api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request', title: 'Test merge_request',
source_branch: 'markdown', source_branch: 'markdown',
target_branch: 'master', target_branch: 'master',
author: user, author: user,
labels: 'label, ?' labels: 'label, label?, label&foo, ?, &'
expect(response).to have_http_status(400) expect(response.status).to eq(201)
expect(json_response['message']['labels']['?']['title']).to eq( expect(json_response['labels']).to include 'label'
['is invalid'] expect(json_response['labels']).to include 'label?'
) expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end end
context 'with existing MR' do context 'with existing MR' do
...@@ -492,13 +494,17 @@ describe API::API, api: true do ...@@ -492,13 +494,17 @@ describe API::API, api: true do
expect(json_response['target_branch']).to eq('wiki') expect(json_response['target_branch']).to eq('wiki')
end end
it 'should return 400 on invalid label names' do it 'should allow special label names' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", put api("/projects/#{project.id}/merge_requests/#{merge_request.id}",
user), user),
title: 'new issue', title: 'new issue',
labels: 'label, ?' labels: 'label, label?, label&foo, ?, &'
expect(response).to have_http_status(400) expect(response.status).to eq(200)
expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
expect(json_response['labels']).to include '?'
expect(json_response['labels']).to include '&'
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment