Commit 33592b8d authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-02-16

parents 708de533 049d2cc4
...@@ -75,6 +75,7 @@ export default class AjaxVariableList { ...@@ -75,6 +75,7 @@ export default class AjaxVariableList {
if (res.status === statusCodes.OK && res.data) { if (res.status === statusCodes.OK && res.data) {
this.updateRowsWithPersistedVariables(res.data.variables); this.updateRowsWithPersistedVariables(res.data.variables);
this.variableList.hideValues();
} else if (res.status === statusCodes.BAD_REQUEST) { } else if (res.status === statusCodes.BAD_REQUEST) {
// Validation failed // Validation failed
this.errorBox.innerHTML = generateErrorBoxContent(res.data); this.errorBox.innerHTML = generateErrorBoxContent(res.data);
......
...@@ -178,6 +178,10 @@ export default class VariableList { ...@@ -178,6 +178,10 @@ export default class VariableList {
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled); this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
} }
hideValues() {
this.secretValues.updateDom(false);
}
getAllData() { getAllData() {
// Ignore the last empty row because we don't want to try persist // Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems. // a blank variable and run into validation problems.
......
...@@ -475,11 +475,6 @@ var Dispatcher; ...@@ -475,11 +475,6 @@ var Dispatcher;
.then(callDefault) .then(callDefault)
.catch(fail); .catch(fail);
break; break;
case 'users:show':
import('./pages/users/show')
.then(callDefault)
.catch(fail);
break;
case 'admin:conversational_development_index:show': case 'admin:conversational_development_index:show':
import('./pages/admin/conversational_development_index/show') import('./pages/admin/conversational_development_index/show')
.then(callDefault) .then(callDefault)
......
...@@ -59,29 +59,36 @@ class ImporterStatus { ...@@ -59,29 +59,36 @@ class ImporterStatus {
.catch(() => flash(__('An error occurred while importing project'))); .catch(() => flash(__('An error occurred while importing project')));
} }
setAutoUpdate() { autoUpdate() {
return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => { return axios.get(this.jobsUrl)
const jobItem = $(`#project_${job.id}`); .then(({ data = [] }) => {
const statusField = jobItem.find('.job-status'); data.forEach((job) => {
const jobItem = $(`#project_${job.id}`);
const statusField = jobItem.find('.job-status');
const spinner = '<i class="fa fa-spinner fa-spin"></i>';
const spinner = '<i class="fa fa-spinner fa-spin"></i>'; switch (job.import_status) {
case 'finished':
jobItem.removeClass('active').addClass('success');
statusField.html('<span><i class="fa fa-check"></i> done</span>');
break;
case 'scheduled':
statusField.html(`${spinner} scheduled`);
break;
case 'started':
statusField.html(`${spinner} started`);
break;
default:
statusField.html(job.import_status);
break;
}
});
});
}
switch (job.import_status) { setAutoUpdate() {
case 'finished': setInterval(this.autoUpdate.bind(this), 4000);
jobItem.removeClass('active').addClass('success');
statusField.html('<span><i class="fa fa-check"></i> done</span>');
break;
case 'scheduled':
statusField.html(`${spinner} scheduled`);
break;
case 'started':
statusField.html(`${spinner} started`);
break;
default:
statusField.html(job.import_status);
break;
}
})), 4000);
} }
} }
......
import UserCallout from '~/user_callout';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import UserTabs from './user_tabs'; import UserTabs from './user_tabs';
...@@ -22,4 +23,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -22,4 +23,5 @@ document.addEventListener('DOMContentLoaded', () => {
const page = $('body').attr('data-page'); const page = $('body').attr('data-page');
const action = page.split(':')[1]; const action = page.split(':')[1];
initUserProfile(action); initUserProfile(action);
new UserCallout(); // eslint-disable-line no-new
}); });
import UserCallout from '~/user_callout';
export default () => new UserCallout();
import axios from '../lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Activities from '../activities'; import Activities from '~/activities';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import flash from '~/flash';
import ActivityCalendar from './activity_calendar'; import ActivityCalendar from './activity_calendar';
import { localTimeAgo } from '../lib/utils/datetime_utility';
import { __ } from '../locale';
import flash from '../flash';
/** /**
* UserTabs * UserTabs
......
...@@ -19,19 +19,10 @@ module Issues ...@@ -19,19 +19,10 @@ module Issues
# on rewriting notes (unfolding references) # on rewriting notes (unfolding references)
# #
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
# New issue tasks
#
@new_issue = create_new_issue @new_issue = create_new_issue
rewrite_notes update_new_issue
rewrite_issue_award_emoji update_old_issue
add_note_moved_from
# Old issue tasks
#
add_note_moved_to
close_issue
mark_as_moved
end end
notify_participants notify_participants
...@@ -41,6 +32,18 @@ module Issues ...@@ -41,6 +32,18 @@ module Issues
private private
def update_new_issue
rewrite_notes
rewrite_issue_award_emoji
add_note_moved_from
end
def update_old_issue
add_note_moved_to
close_issue
mark_as_moved
end
def create_new_issue def create_new_issue
new_params = { id: nil, iid: nil, label_ids: cloneable_label_ids, new_params = { id: nil, iid: nil, label_ids: cloneable_label_ids,
milestone_id: cloneable_milestone_id, milestone_id: cloneable_milestone_id,
......
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
- page_description @user.bio - page_description @user.bio
- header_title @user.name, user_path(@user) - header_title @user.name, user_path(@user)
- @no_container = true - @no_container = true
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_d3'
= webpack_bundle_tag 'users'
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
......
---
title: Hide CI secret variable values after saving
merge_request: 17044
author:
type: changed
---
title: Escape HTML entities in commit messages
merge_request:
author:
type: fixed
...@@ -114,7 +114,6 @@ var config = { ...@@ -114,7 +114,6 @@ var config = {
vue_merge_request_widget: './vue_merge_request_widget/index.js', vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js', test: './test.js',
two_factor_auth: './two_factor_auth.js', two_factor_auth: './two_factor_auth.js',
users: './users/index.js',
webpack_runtime: './webpack.js', webpack_runtime: './webpack.js',
}, },
......
...@@ -27,6 +27,17 @@ Gitlab::Profiler.profile('/my-user') ...@@ -27,6 +27,17 @@ Gitlab::Profiler.profile('/my-user')
# Returns a RubyProf::Profile where 100 seconds is spent in UsersController#show # Returns a RubyProf::Profile where 100 seconds is spent in UsersController#show
``` ```
For routes that require authorization you will need to provide a user to
`Gitlab::Profiler`. You can do this like so:
```ruby
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first)
```
The user you provide will need to have a [personal access
token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html) in
the GitLab instance.
Passing a `logger:` keyword argument to `Gitlab::Profiler.profile` will send Passing a `logger:` keyword argument to `Gitlab::Profiler.profile` will send
ActiveRecord and ActionController log output to that logger. Further options are ActiveRecord and ActionController log output to that logger. Further options are
documented with the method source. documented with the method source.
......
...@@ -5,7 +5,7 @@ module Banzai ...@@ -5,7 +5,7 @@ module Banzai
# Text filter that escapes these HTML entities: & " < > # Text filter that escapes these HTML entities: & " < >
class HtmlEntityFilter < HTML::Pipeline::TextFilter class HtmlEntityFilter < HTML::Pipeline::TextFilter
def call def call
ERB::Util.html_escape_once(text) ERB::Util.html_escape(text)
end end
end end
end end
......
...@@ -45,6 +45,7 @@ module Gitlab ...@@ -45,6 +45,7 @@ module Gitlab
if user if user
private_token ||= user.personal_access_tokens.active.pluck(:token).first private_token ||= user.personal_access_tokens.active.pluck(:token).first
raise 'Your user must have a personal_access_token' unless private_token
end end
headers['Private-Token'] = private_token if private_token headers['Private-Token'] = private_token if private_token
......
...@@ -20,5 +20,9 @@ describe EventsHelper do ...@@ -20,5 +20,9 @@ describe EventsHelper do
it 'handles nil values' do it 'handles nil values' do
expect(helper.event_commit_title(nil)).to eq('') expect(helper.event_commit_title(nil)).to eq('')
end end
it 'does not escape HTML entities' do
expect(helper.event_commit_title("foo & bar")).to eq("foo & bar")
end
end end
end end
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list'; import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list';
const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/variables'; const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/variables';
const HIDE_CLASS = 'hide';
describe('AjaxFormVariableList', () => { describe('AjaxFormVariableList', () => {
preloadFixtures('projects/ci_cd_settings.html.raw'); preloadFixtures('projects/ci_cd_settings.html.raw');
...@@ -45,16 +47,16 @@ describe('AjaxFormVariableList', () => { ...@@ -45,16 +47,16 @@ describe('AjaxFormVariableList', () => {
const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon'); const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon');
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => { mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
expect(loadingIcon.classList.contains('hide')).toEqual(false); expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false);
return [200, {}]; return [200, {}];
}); });
expect(loadingIcon.classList.contains('hide')).toEqual(true); expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked() ajaxVariableList.onSaveClicked()
.then(() => { .then(() => {
expect(loadingIcon.classList.contains('hide')).toEqual(true); expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
...@@ -78,11 +80,11 @@ describe('AjaxFormVariableList', () => { ...@@ -78,11 +80,11 @@ describe('AjaxFormVariableList', () => {
it('hides any previous error box', (done) => { it('hides any previous error box', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200); mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200);
expect(errorBox.classList.contains('hide')).toEqual(true); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked() ajaxVariableList.onSaveClicked()
.then(() => { .then(() => {
expect(errorBox.classList.contains('hide')).toEqual(true); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
...@@ -103,17 +105,39 @@ describe('AjaxFormVariableList', () => { ...@@ -103,17 +105,39 @@ describe('AjaxFormVariableList', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('hides secret values', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {});
const row = container.querySelector('.js-row:first-child');
const valueInput = row.querySelector('.js-ci-variable-input-value');
const valuePlaceholder = row.querySelector('.js-secret-value-placeholder');
valueInput.value = 'bar';
$(valueInput).trigger('input');
expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(true);
expect(valueInput.classList.contains(HIDE_CLASS)).toBe(false);
ajaxVariableList.onSaveClicked()
.then(() => {
expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(false);
expect(valueInput.classList.contains(HIDE_CLASS)).toBe(true);
})
.then(done)
.catch(done.fail);
});
it('shows error box with validation errors', (done) => { it('shows error box with validation errors', (done) => {
const validationError = 'some validation error'; const validationError = 'some validation error';
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [ mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [
validationError, validationError,
]); ]);
expect(errorBox.classList.contains('hide')).toEqual(true); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked() ajaxVariableList.onSaveClicked()
.then(() => { .then(() => {
expect(errorBox.classList.contains('hide')).toEqual(false); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(false);
expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(`Validation failed ${validationError}`); expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(`Validation failed ${validationError}`);
}) })
.then(done) .then(done)
...@@ -123,11 +147,11 @@ describe('AjaxFormVariableList', () => { ...@@ -123,11 +147,11 @@ describe('AjaxFormVariableList', () => {
it('shows flash message when request fails', (done) => { it('shows flash message when request fails', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500); mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500);
expect(errorBox.classList.contains('hide')).toEqual(true); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked() ajaxVariableList.onSaveClicked()
.then(() => { .then(() => {
expect(errorBox.classList.contains('hide')).toEqual(true); expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
...@@ -170,9 +194,9 @@ describe('AjaxFormVariableList', () => { ...@@ -170,9 +194,9 @@ describe('AjaxFormVariableList', () => {
const valueInput = row.querySelector('.js-ci-variable-input-value'); const valueInput = row.querySelector('.js-ci-variable-input-value');
keyInput.value = 'foo'; keyInput.value = 'foo';
keyInput.dispatchEvent(new Event('input')); $(keyInput).trigger('input');
valueInput.value = 'bar'; valueInput.value = 'bar';
valueInput.dispatchEvent(new Event('input')); $(valueInput).trigger('input');
expect(idInput.value).toEqual(''); expect(idInput.value).toEqual('');
......
import VariableList from '~/ci_variable_list/ci_variable_list'; import VariableList from '~/ci_variable_list/ci_variable_list';
import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
const HIDE_CLASS = 'hide';
describe('VariableList', () => { describe('VariableList', () => {
preloadFixtures('pipeline_schedules/edit.html.raw'); preloadFixtures('pipeline_schedules/edit.html.raw');
preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); preloadFixtures('pipeline_schedules/edit_with_variables.html.raw');
...@@ -92,14 +94,14 @@ describe('VariableList', () => { ...@@ -92,14 +94,14 @@ describe('VariableList', () => {
const $inputValue = $row.find('.js-ci-variable-input-value'); const $inputValue = $row.find('.js-ci-variable-input-value');
const $placeholder = $row.find('.js-secret-value-placeholder'); const $placeholder = $row.find('.js-secret-value-placeholder');
expect($placeholder.hasClass('hide')).toBe(false); expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
expect($inputValue.hasClass('hide')).toBe(true); expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
// Reveal values // Reveal values
$wrapper.find('.js-secret-value-reveal-button').click(); $wrapper.find('.js-secret-value-reveal-button').click();
expect($placeholder.hasClass('hide')).toBe(true); expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
expect($inputValue.hasClass('hide')).toBe(false); expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
}); });
}); });
}); });
...@@ -179,4 +181,35 @@ describe('VariableList', () => { ...@@ -179,4 +181,35 @@ describe('VariableList', () => {
expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3); expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
}); });
}); });
describe('hideValues', () => {
beforeEach(() => {
loadFixtures('projects/ci_cd_settings.html.raw');
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
container: $wrapper,
formField: 'variables',
});
variableList.init();
});
it('should hide value input and show placeholder stars', () => {
const $row = $wrapper.find('.js-row');
const $inputValue = $row.find('.js-ci-variable-input-value');
const $placeholder = $row.find('.js-secret-value-placeholder');
$row.find('.js-ci-variable-input-value')
.val('foo')
.trigger('input');
expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
variableList.hideValues();
expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
});
});
}); });
...@@ -3,9 +3,18 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -3,9 +3,18 @@ import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
describe('Importer Status', () => { describe('Importer Status', () => {
let instance;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('addToImport', () => { describe('addToImport', () => {
let instance;
let mock;
const importUrl = '/import_url'; const importUrl = '/import_url';
beforeEach(() => { beforeEach(() => {
...@@ -21,11 +30,6 @@ describe('Importer Status', () => { ...@@ -21,11 +30,6 @@ describe('Importer Status', () => {
spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {}); spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {});
spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {}); spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {});
instance = new ImporterStatus('', importUrl); instance = new ImporterStatus('', importUrl);
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
}); });
it('sets table row to active after post request', (done) => { it('sets table row to active after post request', (done) => {
...@@ -44,4 +48,60 @@ describe('Importer Status', () => { ...@@ -44,4 +48,60 @@ describe('Importer Status', () => {
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('autoUpdate', () => {
const jobsUrl = '/jobs_url';
beforeEach(() => {
const div = document.createElement('div');
div.innerHTML = `
<div id="project_1">
<div class="job-status">
</div>
</div>
`;
document.body.appendChild(div);
spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {});
spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {});
instance = new ImporterStatus(jobsUrl);
});
function setupMock(importStatus) {
mock.onGet(jobsUrl).reply(200, [{
id: 1,
import_status: importStatus,
}]);
}
function expectJobStatus(done, status) {
instance.autoUpdate()
.then(() => {
expect(document.querySelector('#project_1').innerText.trim()).toEqual(status);
done();
})
.catch(done.fail);
}
it('sets the job status to done', (done) => {
setupMock('finished');
expectJobStatus(done, 'done');
});
it('sets the job status to scheduled', (done) => {
setupMock('scheduled');
expectJobStatus(done, 'scheduled');
});
it('sets the job status to started', (done) => {
setupMock('started');
expectJobStatus(done, 'started');
});
it('sets the job status to custom status', (done) => {
setupMock('custom status');
expectJobStatus(done, 'custom status');
});
});
}); });
...@@ -3,17 +3,12 @@ require 'spec_helper' ...@@ -3,17 +3,12 @@ require 'spec_helper'
describe Banzai::Filter::HtmlEntityFilter do describe Banzai::Filter::HtmlEntityFilter do
include FilterSpecHelper include FilterSpecHelper
let(:unescaped) { 'foo <strike attr="foo">&&&</strike>' } let(:unescaped) { 'foo <strike attr="foo">&&amp;&</strike>' }
let(:escaped) { 'foo &lt;strike attr=&quot;foo&quot;&gt;&amp;&amp;&amp;&lt;/strike&gt;' } let(:escaped) { 'foo &lt;strike attr=&quot;foo&quot;&gt;&amp;&amp;amp;&amp;&lt;/strike&gt;' }
it 'converts common entities to their HTML-escaped equivalents' do it 'converts common entities to their HTML-escaped equivalents' do
output = filter(unescaped) output = filter(unescaped)
expect(output).to eq(escaped) expect(output).to eq(escaped)
end end
it 'does not double-escape' do
escaped = ERB::Util.html_escape("Merge branch 'blabla' into 'master'")
expect(filter(escaped)).to eq(escaped)
end
end end
...@@ -53,6 +53,15 @@ describe Gitlab::Profiler do ...@@ -53,6 +53,15 @@ describe Gitlab::Profiler do
described_class.profile('/', user: user) described_class.profile('/', user: user)
end end
context 'when providing a user without a personal access token' do
it 'raises an error' do
user = double(:user)
allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck).and_return([])
expect { described_class.profile('/', user: user) }.to raise_error('Your user must have a personal_access_token')
end
end
it 'uses the private_token for auth if both it and user are set' do it 'uses the private_token for auth if both it and user are set' do
user = double(:user) user = double(:user)
user_token = 'user' user_token = 'user'
......
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