Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-23 06:09:45 +00:00
parent 8007620dc7
commit 86fa823611
31 changed files with 325 additions and 48 deletions

2
.gitignore vendored
View file

@ -96,4 +96,4 @@ apollo.config.js
/tmp/matching_foss_tests.txt
/tmp/matching_tests.txt
ee/changelogs/unreleased-ee
/sitespeed-result

View file

@ -572,7 +572,7 @@ export class AwardsHandler {
}
findMatchingEmojiElements(query) {
const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
const emojiMatches = this.emoji.queryEmojiNames(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements.filter(
(i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,

View file

@ -1,6 +1,5 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlIcon } from '@gitlab/ui';
import { GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg';
import limitWarning from './limit_warning_component.vue';
@ -13,6 +12,9 @@ export default {
limitWarning,
GlIcon,
},
directives: {
SafeHtml,
},
props: {
items: {
type: Array,
@ -47,7 +49,7 @@ export default {
<a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
<gl-icon :size="16" name="fork" />
<a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a>
<span class="icon-branch" v-html="iconBranch"> </span>
<span v-safe-html="iconBranch" class="icon-branch"> </span>
<a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
</h5>
<span>

View file

@ -1,4 +1,5 @@
import { uniq } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import emojiAliases from 'emojis/aliases.json';
import axios from '../lib/utils/axios_utils';
@ -62,13 +63,18 @@ export function isEmojiNameValid(name) {
return validEmojiNames.indexOf(name) >= 0;
}
export function filterEmojiNames(filter) {
const match = filter.toLowerCase();
return validEmojiNames.filter(name => name.indexOf(match) >= 0);
}
export function filterEmojiNamesByAlias(filter) {
return uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name)));
/**
* Search emoji by name or alias. Returns a normalized, deduplicated list of
* names.
*
* Calling with an empty filter returns an empty array.
*
* @param {String}
* @returns {Array}
*/
export function queryEmojiNames(filter) {
const matches = fuzzaldrinPlus.filter(validEmojiNames, filter);
return uniq(matches.map(name => normalizeEmojiName(name)));
}
let emojiCategoryMap;

View file

@ -1,6 +1,6 @@
import { take } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());

View file

@ -1,10 +1,14 @@
<script>
import { GlLink } from '@gitlab/ui';
import { GlLink, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
import { formatDate } from '~/lib/utils/datetime_utility';
export default {
components: {
GlLink,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
alert: {
@ -24,17 +28,23 @@ export default {
<div
class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between"
>
<div class="text-truncate gl-pr-3">
<div class="gl-pr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span>
<gl-link :href="alert.detailsUrl">{{ alert.title }}</gl-link>
<gl-link v-gl-tooltip :title="alert.title" :href="alert.detailsUrl">
<gl-sprintf :message="__('Alert #%{alertId}')">
<template #alertId>
<span>{{ alert.iid }}</span>
</template>
</gl-sprintf>
</gl-link>
</div>
<div class="gl-pr-3 gl-white-space-nowrap">
<div class="gl-pr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert start time:') }}</span>
{{ startTime }}
</div>
<div class="gl-white-space-nowrap">
<div>
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span>
<span>{{ alert.eventCount }}</span>
</div>

View file

@ -1,4 +1,4 @@
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import updateDescription from '../utils/update_description';

View file

@ -1,4 +1,4 @@
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
// We currently load + parse the data from the issue app and related merge request
let cachedParsedData;

View file

@ -0,0 +1,53 @@
import { sanitize as dompurifySanitize, addHook } from 'dompurify';
import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
// Safely allow SVG <use> tags
const defaultConfig = {
ADD_TAGS: ['use'],
};
// Only icons urls from `gon` are allowed
const getAllowedIconUrls = (gon = window.gon) =>
[gon.sprite_file_icons, gon.sprite_icons].filter(Boolean);
const isUrlAllowed = url => getAllowedIconUrls().some(allowedUrl => url.startsWith(allowedUrl));
const isHrefSafe = url =>
isUrlAllowed(url) || isUrlAllowed(relativePathToAbsolute(url, getBaseURL()));
const removeUnsafeHref = (node, attr) => {
if (!node.hasAttribute(attr)) {
return;
}
if (!isHrefSafe(node.getAttribute(attr))) {
node.removeAttribute(attr);
}
};
/**
* Sanitize icons' <use> tag attributes, to safely include
* svgs such as in:
*
* <svg viewBox="0 0 100 100">
* <use href="/assets/icons-xxx.svg#icon_name"></use>
* </svg>
*
* @param {Object} node - Node to sanitize
*/
const sanitizeSvgIcon = node => {
removeUnsafeHref(node, 'href');
// Note: `xlink:href` is deprecated, but still in use
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
removeUnsafeHref(node, 'xlink:href');
};
addHook('afterSanitizeAttributes', node => {
if (node.tagName.toLowerCase() === 'use') {
sanitizeSvgIcon(node);
}
});
export const sanitize = (val, config = defaultConfig) => dompurifySanitize(val, config);

View file

@ -1,5 +1,5 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
/**
* Wraps substring matches with HTML `<span>` elements.

View file

@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
import marked from 'marked';
import { sanitize } from 'dompurify';
import katex from 'katex';
import { sanitize } from '~/lib/dompurify';
import Prompt from './prompt.vue';
const renderer = new marked.Renderer();

View file

@ -1,6 +1,6 @@
<script>
/* eslint-disable vue/no-v-html */
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import Prompt from '../prompt.vue';
export default {

View file

@ -2,7 +2,7 @@
import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash';

View file

@ -1,5 +1,5 @@
<script>
import { GlTooltipDirective, GlLink, GlBadge, GlButton, GlIcon } from '@gitlab/ui';
import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { BACK_URL_PARAM } from '~/releases/constants';
import { setUrlParams } from '~/lib/utils/url_utility';
@ -8,7 +8,6 @@ export default {
components: {
GlLink,
GlBadge,
GlIcon,
GlButton,
},
directives: {
@ -55,11 +54,10 @@ export default {
v-gl-tooltip
category="primary"
variant="default"
icon="pencil"
class="gl-mr-3 js-edit-button ml-2 pb-2"
:title="__('Edit this release')"
:href="editLink"
>
<gl-icon name="pencil" />
</gl-button>
/>
</div>
</template>

View file

@ -1,6 +1,6 @@
import Vue from 'vue';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';

View file

@ -211,6 +211,14 @@
:weight: 1
:idempotent:
:tags: []
- :name: cronjob:member_invitation_reminder_emails
:feature_category: :subgroups
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
- :name: cronjob:metrics_dashboard_schedule_annotations_prune
:feature_category: :metrics
:has_external_dependencies:

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class MemberInvitationReminderEmailsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :subgroups
urgency :low
def perform
return unless Gitlab::Experimentation.enabled?(:invitation_reminders)
# To keep this MR small, implementation will be done in another MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42981/diffs?commit_id=8063606e0f83957b2dd38d660ee986f24dee6138
end
end

View file

@ -0,0 +1,5 @@
---
title: Surface Alert number GFM reference in highlight bar
merge_request: 42832
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Use fuzzy matching for issuable awards
merge_request: 42674
author: Ethan Reesor (@firelizzard)
type: added

View file

@ -0,0 +1,5 @@
---
title: Fix size of edit button on releases page
merge_request: 42779
author:
type: fixed

View file

@ -517,6 +517,9 @@ Settings.cron_jobs['ci_platform_metrics_update_cron_worker']['job_class'] = 'CiP
Settings.cron_jobs['analytics_instance_statistics_count_job_trigger_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['analytics_instance_statistics_count_job_trigger_worker']['cron'] ||= '50 23 */1 * *'
Settings.cron_jobs['analytics_instance_statistics_count_job_trigger_worker']['job_class'] ||= 'Analytics::InstanceStatistics::CountJobTriggerWorker'
Settings.cron_jobs['member_invitation_reminder_emails_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['member_invitation_reminder_emails_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['member_invitation_reminder_emails_worker']['job_class'] = 'MemberInvitationReminderEmailsWorker'
Gitlab.ee do
Settings.cron_jobs['adjourned_group_deletion_worker'] ||= Settingslogic.new({})

View file

@ -432,7 +432,7 @@ To avoid this error, use the applicable HTML entity code (`&lt;` or `&gt;`) inst
- In JavaScript:
```javascript
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
const i18n = { LESS_THAN_ONE_HOUR: sanitize(__('In &lt; 1 hour'), { ALLOWED_TAGS: [] }) };

View file

@ -176,7 +176,7 @@ module Gitlab
name: name.presence || concurrent_foreign_key_name(source, column)
}
if foreign_key_exists?(source, target, options)
if foreign_key_exists?(source, target, **options)
warning_message = "Foreign key not created because it exists already " \
"(this may be due to an aborted migration or similar): " \
"source: #{source}, target: #{target}, column: #{options[:column]}, "\
@ -330,13 +330,13 @@ module Gitlab
# * +timing_configuration+ - [[ActiveSupport::Duration, ActiveSupport::Duration], ...] lock timeout for the block, sleep time before the next iteration, defaults to `Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION`
# * +logger+ - [Gitlab::JsonLogger]
# * +env+ - [Hash] custom environment hash, see the example with `DISABLE_LOCK_RETRIES`
def with_lock_retries(**args, &block)
def with_lock_retries(*args, **kwargs, &block)
merged_args = {
klass: self.class,
logger: Gitlab::BackgroundMigration::Logger
}.merge(args)
}.merge(kwargs)
Gitlab::Database::WithLockRetries.new(merged_args).run(&block)
Gitlab::Database::WithLockRetries.new(**merged_args).run(&block)
end
def true_value

View file

@ -2182,6 +2182,9 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
msgid "Alert #%{alertId}"
msgstr ""
msgid "AlertManagement|Acknowledged"
msgstr ""

View file

@ -309,6 +309,16 @@ describe('AwardsHandler', () => {
expect($('[data-name=alien]').is(':visible')).toBe(true);
expect($('.js-emoji-menu-search').val()).toBe('');
});
it('should fuzzy filter the emoji', async () => {
await openAndWaitForEmojiMenu();
awardsHandler.searchEmojis('sgls');
expect($('[data-name=angel]').is(':visible')).toBe(false);
expect($('[data-name=anger]').is(':visible')).toBe(false);
expect($('[data-name=sunglasses]').is(':visible')).toBe(true);
});
});
describe('emoji menu', () => {

View file

@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { trimText } from 'helpers/text_helper';
import axios from '~/lib/utils/axios_utils';
import { initEmojiMap, glEmojiTag, EMOJI_VERSION } from '~/emoji';
import { initEmojiMap, glEmojiTag, queryEmojiNames, EMOJI_VERSION } from '~/emoji';
import isEmojiUnicodeSupported, {
isFlagEmoji,
isRainbowFlagEmoji,
@ -57,8 +57,15 @@ describe('gl_emoji', () => {
let mock;
beforeEach(() => {
const emojiData = Object.fromEntries(
Object.values(emojiFixtureMap).map(m => {
const { name: n, moji: e, unicodeVersion: u, category: c, description: d } = m;
return [n, { c, e, d, u }];
}),
);
mock = new MockAdapter(axios);
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200);
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, JSON.stringify(emojiData));
return initEmojiMap().catch(() => {});
});
@ -378,4 +385,15 @@ describe('gl_emoji', () => {
expect(isSupported).toBeFalsy();
});
});
describe('queryEmojiNames', () => {
const contains = (e, term) => {
const names = queryEmojiNames(term);
expect(names.indexOf(e.name) >= 0).toBe(true);
};
it('should match by name', () => contains(emojiFixtureMap.grey_question, 'grey_question'));
it('should match by partial name', () => contains(emojiFixtureMap.grey_question, 'question'));
it('should fuzzy match by name', () => contains(emojiFixtureMap.grey_question, 'grqtn'));
});
});

View file

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { GlLink, GlSprintf } from '@gitlab/ui';
import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
@ -9,6 +9,7 @@ describe('Highlight Bar', () => {
let wrapper;
const alert = {
iid: 1,
startedAt: '2020-05-29T10:39:22Z',
detailsUrl: 'http://127.0.0.1:3000/root/unique-alerts/-/alert_management/1/details',
eventCount: 1,
@ -20,6 +21,9 @@ describe('Highlight Bar', () => {
propsData: {
alert,
},
stubs: {
GlSprintf,
},
});
};
@ -39,7 +43,8 @@ describe('Highlight Bar', () => {
it('renders a link to the alert page', () => {
expect(findLink().exists()).toBe(true);
expect(findLink().attributes('href')).toBe(alert.detailsUrl);
expect(findLink().text()).toContain(alert.title);
expect(findLink().attributes('title')).toBe(alert.title);
expect(findLink().text()).toBe(`Alert #${alert.iid}`);
});
it('renders formatted start time of the alert', () => {

View file

@ -0,0 +1,98 @@
import { sanitize } from '~/lib/dompurify';
// GDK
const rootGon = {
sprite_file_icons: '/assets/icons-123a.svg',
sprite_icons: '/assets/icons-456b.svg',
};
// Production
const absoluteGon = {
sprite_file_icons: `${window.location.protocol}//${window.location.hostname}/assets/icons-123a.svg`,
sprite_icons: `${window.location.protocol}//${window.location.hostname}/assets/icons-456b.svg`,
};
const expectedSanitized = '<svg><use></use></svg>';
const safeUrls = {
root: Object.values(rootGon).map(url => `${url}#ellipsis_h`),
absolute: Object.values(absoluteGon).map(url => `${url}#ellipsis_h`),
};
const unsafeUrls = [
'/an/evil/url',
'../../../evil/url',
'https://evil.url/assets/icons-123a.svg',
'https://evil.url/assets/icons-456b.svg',
`https://evil.url/${rootGon.sprite_icons}`,
`https://evil.url/${rootGon.sprite_file_icons}`,
`https://evil.url/${absoluteGon.sprite_icons}`,
`https://evil.url/${absoluteGon.sprite_file_icons}`,
];
describe('~/lib/dompurify', () => {
let originalGon;
it('uses local configuration when given', () => {
// As dompurify uses a "Persistent Configuration", it might
// ignore config, this check verifies we respect
// https://github.com/cure53/DOMPurify#persistent-configuration
expect(sanitize('<br>', { ALLOWED_TAGS: [] })).toBe('');
expect(sanitize('<strong></strong>', { ALLOWED_TAGS: [] })).toBe('');
});
describe.each`
type | gon
${'root'} | ${rootGon}
${'absolute'} | ${absoluteGon}
`('when gon contains $type icon urls', ({ type, gon }) => {
beforeAll(() => {
originalGon = window.gon;
window.gon = gon;
});
afterAll(() => {
window.gon = originalGon;
});
it('allows no href attrs', () => {
const htmlHref = `<svg><use></use></svg>`;
expect(sanitize(htmlHref)).toBe(htmlHref);
});
it.each(safeUrls[type])('allows safe URL %s', url => {
const htmlHref = `<svg><use href="${url}"></use></svg>`;
expect(sanitize(htmlHref)).toBe(htmlHref);
const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
expect(sanitize(htmlXlink)).toBe(htmlXlink);
});
it.each(unsafeUrls)('sanitizes unsafe URL %s', url => {
const htmlHref = `<svg><use href="${url}"></use></svg>`;
const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
expect(sanitize(htmlHref)).toBe(expectedSanitized);
expect(sanitize(htmlXlink)).toBe(expectedSanitized);
});
});
describe('when gon does not contain icon urls', () => {
beforeAll(() => {
originalGon = window.gon;
window.gon = {};
});
afterAll(() => {
window.gon = originalGon;
});
it.each([...safeUrls.root, ...safeUrls.absolute, ...unsafeUrls])('sanitizes URL %s', url => {
const htmlHref = `<svg><use href="${url}"></use></svg>`;
const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
expect(sanitize(htmlHref)).toBe(expectedSanitized);
expect(sanitize(htmlXlink)).toBe(expectedSanitized);
});
});
});

View file

@ -1,11 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
import { sanitize } from 'dompurify';
import { sanitize } from '~/lib/dompurify';
import ProjectFindFile from '~/project_find_file';
import axios from '~/lib/utils/axios_utils';
jest.mock('dompurify', () => ({
jest.mock('~/lib/dompurify', () => ({
addHook: jest.fn(),
sanitize: jest.fn(val => val),
}));

View file

@ -5,17 +5,20 @@ require 'spec_helper'
RSpec.describe Ci::PipelinePresenter do
include Gitlab::Routing
let(:user) { create(:user) }
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :test_repo) }
let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }
let(:current_user) { user }
let(:project) { create(:project, :test_repo) }
let(:pipeline) { create(:ci_pipeline, project: project) }
subject(:presenter) do
described_class.new(pipeline)
end
before do
before_all do
project.add_developer(user)
end
before do
allow(presenter).to receive(:current_user) { current_user }
end
@ -184,8 +187,8 @@ RSpec.describe Ci::PipelinePresenter do
describe '#all_related_merge_request_text' do
subject { presenter.all_related_merge_request_text }
let(:mr_1) { create(:merge_request) }
let(:mr_2) { create(:merge_request) }
let_it_be(:mr_1) { create(:merge_request) }
let_it_be(:mr_2) { create(:merge_request) }
context 'with zero related merge requests (branch pipeline)' do
it { is_expected.to eq('No related merge requests found.') }
@ -242,7 +245,7 @@ RSpec.describe Ci::PipelinePresenter do
end
context 'permissions' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
let_it_be_with_refind(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
let(:pipeline) { merge_request.all_pipelines.take }
shared_examples 'private merge requests' do

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MemberInvitationReminderEmailsWorker do
describe '#perform' do
subject { described_class.new.perform }
context 'feature flag disabled' do
before do
stub_experiment(invitation_reminders: false)
end
it 'does not raise an error' do
expect { subject }.not_to raise_error
end
end
context 'feature flag enabled' do
before do
stub_experiment(invitation_reminders: true)
end
it 'does not raise an error' do
expect { subject }.not_to raise_error
end
end
end
end