Commit 0a850868 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 3744bcc0
...@@ -21,6 +21,7 @@ import Reference from './nodes/reference'; ...@@ -21,6 +21,7 @@ import Reference from './nodes/reference';
import TableOfContents from './nodes/table_of_contents'; import TableOfContents from './nodes/table_of_contents';
import Video from './nodes/video'; import Video from './nodes/video';
import Audio from './nodes/audio';
import BulletList from './nodes/bullet_list'; import BulletList from './nodes/bullet_list';
import OrderedList from './nodes/ordered_list'; import OrderedList from './nodes/ordered_list';
...@@ -78,6 +79,7 @@ export default [ ...@@ -78,6 +79,7 @@ export default [
new TableOfContents(), new TableOfContents(),
new Video(), new Video(),
new Audio(),
new BulletList(), new BulletList(),
new OrderedList(), new OrderedList(),
......
/* eslint-disable class-methods-use-this */
import { Node } from 'tiptap';
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
// Transforms generated HTML back to GFM for Banzai::Filter::AudioLinkFilter
export default class Audio extends Node {
get name() {
return 'audio';
}
get schema() {
return {
attrs: {
src: {},
alt: {
default: null,
},
},
group: 'block',
draggable: true,
parseDOM: [
{
tag: '.audio-container',
skip: true,
},
{
tag: '.audio-container p',
priority: 51,
ignore: true,
},
{
tag: 'audio[src]',
getAttrs: el => ({ src: el.getAttribute('src'), alt: el.dataset.title }),
},
],
toDOM: node => [
'audio',
{
src: node.attrs.src,
controls: true,
'data-setup': '{}',
'data-title': node.attrs.alt,
},
],
};
}
toMarkdown(state, node) {
defaultMarkdownSerializer.nodes.image(state, node);
state.closeBlock(node);
}
}
...@@ -107,18 +107,18 @@ export default class BlobViewer { ...@@ -107,18 +107,18 @@ export default class BlobViewer {
toggleCopyButtonState() { toggleCopyButtonState() {
if (!this.copySourceBtn) return; if (!this.copySourceBtn) return;
if (this.simpleViewer.getAttribute('data-loaded')) { if (this.simpleViewer.getAttribute('data-loaded')) {
this.copySourceBtn.setAttribute('title', __('Copy source to clipboard')); this.copySourceBtn.setAttribute('title', __('Copy file contents'));
this.copySourceBtn.classList.remove('disabled'); this.copySourceBtn.classList.remove('disabled');
} else if (this.activeViewer === this.simpleViewer) { } else if (this.activeViewer === this.simpleViewer) {
this.copySourceBtn.setAttribute( this.copySourceBtn.setAttribute(
'title', 'title',
__('Wait for the source to load to copy it to the clipboard'), __('Wait for the file to load to copy its contents'),
); );
this.copySourceBtn.classList.add('disabled'); this.copySourceBtn.classList.add('disabled');
} else { } else {
this.copySourceBtn.setAttribute( this.copySourceBtn.setAttribute(
'title', 'title',
__('Switch to the source to copy it to the clipboard'), __('Switch to the source to copy the file contents'),
); );
this.copySourceBtn.classList.add('disabled'); this.copySourceBtn.classList.add('disabled');
} }
......
...@@ -294,7 +294,7 @@ export default { ...@@ -294,7 +294,7 @@ export default {
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="ingressExternalEndpoint" :text="ingressExternalEndpoint"
:title="s__('ClusterIntegration|Copy Ingress Endpoint to clipboard')" :title="s__('ClusterIntegration|Copy Ingress Endpoint')"
class="input-group-text js-clipboard-btn" class="input-group-text js-clipboard-btn"
/> />
</span> </span>
...@@ -472,7 +472,7 @@ export default { ...@@ -472,7 +472,7 @@ export default {
<span class="input-group-btn"> <span class="input-group-btn">
<clipboard-button <clipboard-button
:text="jupyterHostname" :text="jupyterHostname"
:title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')" :title="s__('ClusterIntegration|Copy Jupyter Hostname')"
class="js-clipboard-btn" class="js-clipboard-btn"
/> />
</span> </span>
......
...@@ -103,7 +103,7 @@ export default { ...@@ -103,7 +103,7 @@ export default {
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="knativeExternalEndpoint" :text="knativeExternalEndpoint"
:title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')" :title="s__('ClusterIntegration|Copy Knative Endpoint')"
class="input-group-text js-knative-endpoint-clipboard-btn" class="input-group-text js-knative-endpoint-clipboard-btn"
/> />
</span> </span>
......
...@@ -121,7 +121,7 @@ export default { ...@@ -121,7 +121,7 @@ export default {
<div class="label label-monospace monospace" v-text="commit.short_id"></div> <div class="label label-monospace monospace" v-text="commit.short_id"></div>
<clipboard-button <clipboard-button
:text="commit.id" :text="commit.id"
:title="__('Copy commit SHA to clipboard')" :title="__('Copy commit SHA')"
class="btn btn-default" class="btn btn-default"
/> />
</div> </div>
......
...@@ -209,7 +209,7 @@ export default { ...@@ -209,7 +209,7 @@ export default {
</a> </a>
<clipboard-button <clipboard-button
:title="__('Copy file path to clipboard')" :title="__('Copy file path')"
:text="diffFile.file_path" :text="diffFile.file_path"
:gfm="gfmCopyText" :gfm="gfmCopyText"
css-class="btn-default btn-transparent btn-clipboard" css-class="btn-default btn-transparent btn-clipboard"
......
...@@ -41,7 +41,7 @@ export default { ...@@ -41,7 +41,7 @@ export default {
<clipboard-button <clipboard-button
:text="commit.id" :text="commit.id"
:title="__('Copy commit SHA to clipboard')" :title="__('Copy commit SHA')"
css-class="btn btn-clipboard btn-transparent" css-class="btn btn-clipboard btn-transparent"
/> />
......
<script> <script>
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import VueDraggable from 'vuedraggable';
import { import {
GlButton, GlButton,
GlDropdown, GlDropdown,
...@@ -8,8 +11,6 @@ import { ...@@ -8,8 +11,6 @@ import {
GlModalDirective, GlModalDirective,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility'; import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
...@@ -26,6 +27,7 @@ let sidebarMutationObserver; ...@@ -26,6 +27,7 @@ let sidebarMutationObserver;
export default { export default {
components: { components: {
VueDraggable,
MonitorTimeSeriesChart, MonitorTimeSeriesChart,
MonitorSingleStatChart, MonitorSingleStatChart,
PanelType, PanelType,
...@@ -151,6 +153,11 @@ export default { ...@@ -151,6 +153,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
rearrangePanelsAvailable: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -160,6 +167,7 @@ export default { ...@@ -160,6 +167,7 @@ export default {
selectedTimeWindowKey: '', selectedTimeWindowKey: '',
formIsValid: null, formIsValid: null,
timeWindows: {}, timeWindows: {},
isRearrangingPanels: false,
}; };
}, },
computed: { computed: {
...@@ -183,6 +191,9 @@ export default { ...@@ -183,6 +191,9 @@ export default {
selectedDashboardText() { selectedDashboardText() {
return this.currentDashboard || this.firstDashboard.display_name; return this.currentDashboard || this.firstDashboard.display_name;
}, },
showRearrangePanelsBtn() {
return !this.showEmptyState && this.rearrangePanelsAvailable;
},
addingMetricsAvailable() { addingMetricsAvailable() {
return IS_EE && this.canAddMetrics && !this.showEmptyState; return IS_EE && this.canAddMetrics && !this.showEmptyState;
}, },
...@@ -271,9 +282,14 @@ export default { ...@@ -271,9 +282,14 @@ export default {
return Object.values(this.getGraphAlerts(queries)); return Object.values(this.getGraphAlerts(queries));
}, },
showToast() { showToast() {
this.$toast.show(__('Link copied to clipboard')); this.$toast.show(__('Link copied'));
}, },
// TODO: END // TODO: END
removeGraph(metrics, graphIndex) {
// At present graphs will not be removed, they should removed using the vuex store
// See https://gitlab.com/gitlab-org/gitlab/issues/27835
metrics.splice(graphIndex, 1);
},
generateLink(group, title, yLabel) { generateLink(group, title, yLabel) {
const dashboard = this.currentDashboard || this.firstDashboard.path; const dashboard = this.currentDashboard || this.firstDashboard.path;
const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null); const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null);
...@@ -287,6 +303,9 @@ export default { ...@@ -287,6 +303,9 @@ export default {
this.elWidth = this.$el.clientWidth; this.elWidth = this.$el.clientWidth;
}, sidebarAnimationDuration); }, sidebarAnimationDuration);
}, },
toggleRearrangingPanels() {
this.isRearrangingPanels = !this.isRearrangingPanels;
},
setFormValidity(isValid) { setFormValidity(isValid) {
this.formIsValid = isValid; this.formIsValid = isValid;
}, },
...@@ -389,15 +408,27 @@ export default { ...@@ -389,15 +408,27 @@ export default {
</template> </template>
<gl-form-group <gl-form-group
v-if="addingMetricsAvailable || externalDashboardUrl.length" v-if="addingMetricsAvailable || showRearrangePanelsBtn || externalDashboardUrl.length"
label-for="prometheus-graphs-dropdown-buttons" label-for="prometheus-graphs-dropdown-buttons"
class="dropdown-buttons col-lg d-lg-flex align-items-end" class="dropdown-buttons col-lg d-lg-flex align-items-end"
> >
<div id="prometheus-graphs-dropdown-buttons"> <div id="prometheus-graphs-dropdown-buttons">
<gl-button
v-if="showRearrangePanelsBtn"
:pressed="isRearrangingPanels"
new-style
variant="default"
class="mr-2 mt-1 js-rearrange-button"
@click="toggleRearrangingPanels"
>
{{ __('Arrange charts') }}
</gl-button>
<gl-button <gl-button
v-if="addingMetricsAvailable" v-if="addingMetricsAvailable"
v-gl-modal="$options.addMetric.modalId" v-gl-modal="$options.addMetric.modalId"
class="mr-2 mt-1 js-add-metric-button text-success border-success" new-style
variant="outline-success"
class="mr-2 mt-1 js-add-metric-button"
> >
{{ $options.addMetric.title }} {{ $options.addMetric.title }}
</gl-button> </gl-button>
...@@ -451,17 +482,42 @@ export default { ...@@ -451,17 +482,42 @@ export default {
:collapse-group="groupHasData(groupData)" :collapse-group="groupHasData(groupData)"
> >
<template v-if="additionalPanelTypesEnabled"> <template v-if="additionalPanelTypesEnabled">
<panel-type <vue-draggable
v-for="(graphData, graphIndex) in groupData.metrics" :list="groupData.metrics"
:key="`panel-type-${graphIndex}`" group="metrics-dashboard"
class="col-12 col-lg-6 pb-3" :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
:clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)" :disabled="!isRearrangingPanels"
:graph-data="graphData" >
:dashboard-width="elWidth" <div
:alerts-endpoint="alertsEndpoint" v-for="(graphData, graphIndex) in groupData.metrics"
:prometheus-alerts-available="prometheusAlertsAvailable" :key="`panel-type-${graphIndex}`"
:index="`${index}-${graphIndex}`" class="col-12 col-lg-6 px-2 mb-2 draggable"
/> :class="{ 'draggable-enabled': isRearrangingPanels }"
>
<div class="position-relative draggable-panel js-draggable-panel">
<div
v-if="isRearrangingPanels"
class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
@click="removeGraph(groupData.metrics, graphIndex)"
>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
><icon name="close"
/></a>
</div>
<panel-type
:clipboard-text="
generateLink(groupData.group, graphData.title, graphData.y_label)
"
:graph-data="graphData"
:dashboard-width="elWidth"
:alerts-endpoint="alertsEndpoint"
:prometheus-alerts-available="prometheusAlertsAvailable"
:index="`${index}-${graphIndex}`"
/>
</div>
</div>
</vue-draggable>
</template> </template>
<template v-else> <template v-else>
<monitor-time-series-chart <monitor-time-series-chart
......
...@@ -52,7 +52,7 @@ export default { ...@@ -52,7 +52,7 @@ export default {
<div <div
v-if="collapseGroup" v-if="collapseGroup"
v-show="collapseGroup && showGroup" v-show="collapseGroup && showGroup"
class="card-body prometheus-graph-group" class="card-body prometheus-graph-group p-0"
> >
<slot></slot> <slot></slot>
</div> </div>
......
...@@ -82,7 +82,7 @@ export default { ...@@ -82,7 +82,7 @@ export default {
return this.graphData.type && this.graphData.type === type; return this.graphData.type && this.graphData.type === type;
}, },
showToast() { showToast() {
this.$toast.show(__('Link copied to clipboard')); this.$toast.show(__('Link copied'));
}, },
}, },
}; };
......
...@@ -143,7 +143,7 @@ export default { ...@@ -143,7 +143,7 @@ export default {
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="dockerBuildCommand" :text="dockerBuildCommand"
:title="s__('ContainerRegistry|Copy build command to clipboard')" :title="s__('ContainerRegistry|Copy build command')"
class="input-group-text" class="input-group-text"
/> />
</span> </span>
...@@ -154,7 +154,7 @@ export default { ...@@ -154,7 +154,7 @@ export default {
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="dockerPushCommand" :text="dockerPushCommand"
:title="s__('ContainerRegistry|Copy push command to clipboard')" :title="s__('ContainerRegistry|Copy push command')"
class="input-group-text" class="input-group-text"
/> />
</span> </span>
......
...@@ -144,7 +144,7 @@ export default { ...@@ -144,7 +144,7 @@ export default {
</div> </div>
<clipboard-button <clipboard-button
:text="commit.sha" :text="commit.sha"
:title="__('Copy commit SHA to clipboard')" :title="__('Copy commit SHA')"
tooltip-placement="bottom" tooltip-placement="bottom"
/> />
</div> </div>
......
...@@ -23,7 +23,7 @@ export default { ...@@ -23,7 +23,7 @@ export default {
<div class="url-text-field label label-monospace monospace">{{ uri }}</div> <div class="url-text-field label label-monospace monospace">{{ uri }}</div>
<clipboard-button <clipboard-button
:text="uri" :text="uri"
:title="s__('ServerlessURL|Copy URL to clipboard')" :title="s__('ServerlessURL|Copy URL')"
class="input-group-text js-clipboard-btn" class="input-group-text js-clipboard-btn"
/> />
<gl-button <gl-button
......
...@@ -90,7 +90,7 @@ export default { ...@@ -90,7 +90,7 @@ export default {
v-html="mr.sourceBranchLink" v-html="mr.sourceBranchLink"
/><clipboard-button /><clipboard-button
:text="branchNameClipboardData" :text="branchNameClipboardData"
:title="__('Copy branch name to clipboard')" :title="__('Copy branch name')"
css-class="btn-default btn-transparent btn-clipboard" css-class="btn-default btn-transparent btn-clipboard"
/> />
{{ s__('mrWidget|into') }} {{ s__('mrWidget|into') }}
......
...@@ -170,7 +170,7 @@ export default { ...@@ -170,7 +170,7 @@ export default {
> >
</a> </a>
<clipboard-button <clipboard-button
:title="__('Copy commit SHA to clipboard')" :title="__('Copy commit SHA')"
:text="mr.mergeCommitSha" :text="mr.mergeCommitSha"
css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha" css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha"
/> />
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
* *
* @example * @example
* <clipboard-button * <clipboard-button
* title="Copy to clipboard" * title="Copy"
* text="Content to be copied" * text="Content to be copied"
* css-class="btn-transparent" * css-class="btn-transparent"
* /> * />
......
...@@ -15,6 +15,37 @@ ...@@ -15,6 +15,37 @@
} }
} }
.draggable {
&.draggable-enabled {
.draggable-panel {
border: $gray-200 1px solid;
border-radius: $border-radius-default;
margin: -1px;
cursor: grab;
}
.prometheus-graph {
// Make dragging easier by disabling use of chart
pointer-events: none;
}
}
&.sortable-chosen .draggable-panel {
background: $white-light;
box-shadow: 0 0 4px $gray-500;
}
.draggable-remove {
z-index: 1;
.draggable-remove-link {
cursor: pointer;
color: $gray-600;
background-color: $white-light;
}
}
}
.prometheus-panel { .prometheus-panel {
margin-top: 20px; margin-top: 20px;
} }
...@@ -22,11 +53,11 @@ ...@@ -22,11 +53,11 @@
.prometheus-graph-group { .prometheus-graph-group {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding: $gl-padding / 2; margin-top: $gl-padding-8;
} }
.prometheus-graph { .prometheus-graph {
padding: $gl-padding / 2; padding: $gl-padding-8;
} }
.prometheus-graph-embed { .prometheus-graph-embed {
......
...@@ -37,7 +37,7 @@ module UploadsActions ...@@ -37,7 +37,7 @@ module UploadsActions
expires_in 0.seconds, must_revalidate: true, private: true expires_in 0.seconds, must_revalidate: true, private: true
end end
disposition = uploader.image_or_video? ? 'inline' : 'attachment' disposition = uploader.embeddable? ? 'inline' : 'attachment'
uploaders = [uploader, *uploader.versions.values] uploaders = [uploader, *uploader.versions.values]
uploader = uploaders.find { |version| version.filename == params[:filename] } uploader = uploaders.find { |version| version.filename == params[:filename] }
...@@ -112,8 +112,8 @@ module UploadsActions ...@@ -112,8 +112,8 @@ module UploadsActions
uploader uploader
end end
def image_or_video? def embeddable?
uploader && uploader.exists? && uploader.image_or_video? uploader && uploader.exists? && uploader.embeddable?
end end
def find_model def find_model
......
...@@ -4,7 +4,7 @@ class Groups::UploadsController < Groups::ApplicationController ...@@ -4,7 +4,7 @@ class Groups::UploadsController < Groups::ApplicationController
include UploadsActions include UploadsActions
include WorkhorseRequest include WorkhorseRequest
skip_before_action :group, if: -> { action_name == 'show' && image_or_video? } skip_before_action :group, if: -> { action_name == 'show' && embeddable? }
before_action :authorize_upload_file!, only: [:create, :authorize] before_action :authorize_upload_file!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize] before_action :verify_workhorse_api!, only: [:authorize]
......
...@@ -40,8 +40,8 @@ class HelpController < ApplicationController ...@@ -40,8 +40,8 @@ class HelpController < ApplicationController
end end
end end
# Allow access to images in the doc folder # Allow access to specific media files in the doc folder
format.any(:png, :gif, :jpeg, :mp4) do format.any(:png, :gif, :jpeg, :mp4, :mp3) do
# Note: We are purposefully NOT using `Rails.root.join` # Note: We are purposefully NOT using `Rails.root.join`
path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}") path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}")
......
...@@ -6,7 +6,7 @@ class Projects::UploadsController < Projects::ApplicationController ...@@ -6,7 +6,7 @@ class Projects::UploadsController < Projects::ApplicationController
# These will kick you out if you don't have access. # These will kick you out if you don't have access.
skip_before_action :project, :repository, skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? } if: -> { action_name == 'show' && embeddable? }
before_action :authorize_upload_file!, only: [:create, :authorize] before_action :authorize_upload_file!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize] before_action :verify_workhorse_api!, only: [:authorize]
......
...@@ -197,13 +197,13 @@ module BlobHelper ...@@ -197,13 +197,13 @@ module BlobHelper
end end
def copy_file_path_button(file_path) def copy_file_path_button(file_path)
clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent', title: 'Copy file path to clipboard') clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent', title: _('Copy file path'))
end end
def copy_blob_source_button(blob) def copy_blob_source_button(blob)
return unless blob.rendered_as_text?(ignore_errors: false) return unless blob.rendered_as_text?(ignore_errors: false)
clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard") clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: _("Copy file contents"))
end end
def open_raw_blob_button(blob) def open_raw_blob_button(blob)
......
...@@ -21,7 +21,7 @@ module ButtonHelper ...@@ -21,7 +21,7 @@ module ButtonHelper
# See http://clipboardjs.com/#usage # See http://clipboardjs.com/#usage
def clipboard_button(data = {}) def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent' css_class = data[:class] || 'btn-clipboard btn-transparent'
title = data[:title] || _('Copy to clipboard') title = data[:title] || _('Copy')
button_text = data[:button_text] || '' button_text = data[:button_text] || ''
hide_tooltip = data[:hide_tooltip] || false hide_tooltip = data[:hide_tooltip] || false
hide_button_icon = data[:hide_button_icon] || false hide_button_icon = data[:hide_button_icon] || false
......
...@@ -79,7 +79,7 @@ module SearchHelper ...@@ -79,7 +79,7 @@ module SearchHelper
def search_entries_empty_message(scope, term) def search_entries_empty_message(scope, term)
(s_("SearchResults|We couldn't find any %{scope} matching %{term}") % { (s_("SearchResults|We couldn't find any %{scope} matching %{term}") % {
scope: search_entries_scope_label(scope, 0), scope: search_entries_scope_label(scope, 0),
term: "<code>#{term}</code>" term: "<code>#{h(term)}</code>"
}).html_safe }).html_safe
end end
......
...@@ -109,6 +109,9 @@ module ApplicationSettingImplementation ...@@ -109,6 +109,9 @@ module ApplicationSettingImplementation
throttle_protected_paths_in_seconds: 10, throttle_protected_paths_in_seconds: 10,
throttle_protected_paths_per_period: 60, throttle_protected_paths_per_period: 60,
protected_paths: DEFAULT_PROTECTED_PATHS, protected_paths: DEFAULT_PROTECTED_PATHS,
throttle_incident_management_notification_enabled: false,
throttle_incident_management_notification_period_in_seconds: 3600,
throttle_incident_management_notification_per_period: 3600,
time_tracking_limit_to_hours: false, time_tracking_limit_to_hours: false,
two_factor_grace_period: 48, two_factor_grace_period: 48,
unique_ips_limit_enabled: false, unique_ips_limit_enabled: false,
......
...@@ -179,6 +179,10 @@ class Blob < SimpleDelegator ...@@ -179,6 +179,10 @@ class Blob < SimpleDelegator
UploaderHelper::SAFE_VIDEO_EXT.include?(extension) UploaderHelper::SAFE_VIDEO_EXT.include?(extension)
end end
def audio?
UploaderHelper::SAFE_AUDIO_EXT.include?(extension)
end
def readable_text? def readable_text?
text_in_repo? && !stored_externally? && !truncated? text_in_repo? && !stored_externally? && !truncated?
end end
......
...@@ -415,7 +415,7 @@ class Commit ...@@ -415,7 +415,7 @@ class Commit
if entry[:type] == :blob if entry[:type] == :blob
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project) blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
blob.image? || blob.video? ? :raw : :blob blob.image? || blob.video? || blob.audio? ? :raw : :blob
else else
entry[:type] entry[:type]
end end
......
...@@ -45,3 +45,5 @@ ...@@ -45,3 +45,5 @@
= _('Configure paths to be protected by Rack Attack. A web server restart is required after changing these settings.') = _('Configure paths to be protected by Rack Attack. A web server restart is required after changing these settings.')
.settings-content .settings-content
= render 'protected_paths' = render 'protected_paths'
= render_if_exists 'admin/application_settings/ee_network_settings'
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
.input-group .input-group
%input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true } %input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
.input-group-append .input-group-append
= clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default") = clipboard_button(target: '#application_id', title: _("Copy ID"), class: "btn btn btn-default")
%tr %tr
%td %td
= _('Secret') = _('Secret')
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
.input-group .input-group
%input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true } %input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
.input-group-append .input-group-append
= clipboard_button(target: '#secret', title: _("Copy secret to clipboard"), class: "btn btn btn-default") = clipboard_button(target: '#secret', title: _("Copy secret"), class: "btn btn btn-default")
%tr %tr
%td %td
= _('Callback URL') = _('Callback URL')
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- if @new_impersonation_token - if @new_impersonation_token
= render "shared/personal_access_tokens_created_container", new_token_value: @new_impersonation_token, = render "shared/personal_access_tokens_created_container", new_token_value: @new_impersonation_token,
container_title: 'Your New Impersonation Token', container_title: 'Your New Impersonation Token',
clipboard_button_title: 'Copy impersonation token to clipboard' clipboard_button_title: _('Copy impersonation token')
= render "shared/personal_access_tokens_form", path: admin_user_impersonation_tokens_path, impersonation: true, token: @impersonation_token, scopes: @scopes = render "shared/personal_access_tokens_form", path: admin_user_impersonation_tokens_path, impersonation: true, token: @impersonation_token, scopes: @scopes
......
...@@ -8,11 +8,11 @@ ...@@ -8,11 +8,11 @@
%li %li
= _("Specify the following URL during the Runner setup:") = _("Specify the following URL during the Runner setup:")
%code#coordinator_address= root_url(only_path: false) %code#coordinator_address= root_url(only_path: false)
= clipboard_button(target: '#coordinator_address', title: _("Copy URL to clipboard"), class: "btn-transparent btn-clipboard") = clipboard_button(target: '#coordinator_address', title: _("Copy URL"), class: "btn-transparent btn-clipboard")
%li %li
= _("Use the following registration token during setup:") = _("Use the following registration token during setup:")
%code#registration_token= registration_token %code#registration_token= registration_token
= clipboard_button(target: '#registration_token', title: _("Copy token to clipboard"), class: "btn-transparent btn-clipboard") = clipboard_button(target: '#registration_token', title: _("Copy token"), class: "btn-transparent btn-clipboard")
.prepend-top-10.append-bottom-10 .prepend-top-10.append-bottom-10
= button_to _("Reset runners registration token"), reset_token_url, = button_to _("Reset runners registration token"), reset_token_url,
method: :put, class: 'btn btn-default', method: :put, class: 'btn btn-default',
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
.input-group .input-group
%input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true } %input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
.input-group-append .input-group-append
= clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default") = clipboard_button(target: '#application_id', title: _("Copy ID"), class: "btn btn btn-default")
%tr %tr
%td %td
= _('Secret') = _('Secret')
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
.input-group .input-group
%input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true } %input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
.input-group-append .input-group-append
= clipboard_button(target: '#secret', title: _("Copy secret to clipboard"), class: "btn btn btn-default") = clipboard_button(target: '#secret', title: _("Copy secret"), class: "btn btn btn-default")
%tr %tr
%td %td
= _('Callback URL') = _('Callback URL')
......
...@@ -6,6 +6,6 @@ ...@@ -6,6 +6,6 @@
%span %span
= text_field_tag :push_to_create_tip, push_to_create_project_command, class: "js-select-on-focus form-control monospace", readonly: true, aria: { label: _("Push project from command line") } = text_field_tag :push_to_create_tip, push_to_create_project_command, class: "js-select-on-focus form-control monospace", readonly: true, aria: { label: _("Push project from command line") }
%span.input-group-append %span.input-group-append
= clipboard_button(text: push_to_create_project_command, title: _("Copy command to clipboard"), class: 'input-group-text', placement: "right") = clipboard_button(text: push_to_create_project_command, title: _("Copy command"), class: 'input-group-text', placement: "right")
%p %p
= link_to("What does this command do?", help_page_path("gitlab-basics/create-project", anchor: "push-to-create-a-new-project"), target: "_blank") = link_to("What does this command do?", help_page_path("gitlab-basics/create-project", anchor: "push-to-create-a-new-project"), target: "_blank")
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
.input-group .input-group
= text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: 'Project clone URL' } = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: 'Project clone URL' }
.input-group-append .input-group-append
= clipboard_button(target: '#ssh_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard") = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo' = render_if_exists 'projects/buttons/geo'
- if http_enabled? - if http_enabled?
%li.pt-2 %li.pt-2
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
.input-group .input-group
= text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control qa-http-clone-url", readonly: true, aria: { label: 'Project clone URL' } = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control qa-http-clone-url", readonly: true, aria: { label: 'Project clone URL' }
.input-group-append .input-group-append
= clipboard_button(target: '#http_project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard") = clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo' = render_if_exists 'projects/buttons/geo'
= render_if_exists 'projects/buttons/kerberos_clone_field' = render_if_exists 'projects/buttons/kerberos_clone_field'
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%strong %strong
#{ s_('CommitBoxTitle|Commit') } #{ s_('CommitBoxTitle|Commit') }
%span.commit-sha{ data: { qa_selector: 'commit_sha_content' } }= @commit.short_id %span.commit-sha{ data: { qa_selector: 'commit_sha_content' } }= @commit.short_id
= clipboard_button(text: @commit.id, title: _('Copy commit SHA to clipboard')) = clipboard_button(text: @commit.id, title: _('Copy commit SHA'))
%span.d-none.d-sm-inline= _('authored') %span.d-none.d-sm-inline= _('authored')
#{time_ago_with_tooltip(@commit.authored_date)} #{time_ago_with_tooltip(@commit.authored_date)}
%span= s_('ByAuthor|by') %span= s_('ByAuthor|by')
......
...@@ -56,5 +56,5 @@ ...@@ -56,5 +56,5 @@
.commit-sha-group.d-none.d-sm-flex .commit-sha-group.d-none.d-sm-flex
.label.label-monospace.monospace .label.label-monospace.monospace
= commit.short_id = commit.short_id
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"), class: "btn btn-default", container: "body") = clipboard_button(text: commit.id, title: _("Copy commit SHA"), class: "btn btn-default", container: "body")
= link_to_browse_code(project, commit) = link_to_browse_code(project, commit)
...@@ -7,12 +7,12 @@ ...@@ -7,12 +7,12 @@
.input-group .input-group
= text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus qa-deploy-token-user' = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus qa-deploy-token-user'
.input-group-append .input-group-append
= clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left') = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username'), placement: 'left')
%span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.") %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.")
.form-group .form-group
.input-group .input-group
= text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus qa-deploy-token' = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus qa-deploy-token'
.input-group-append .input-group-append
= clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left') = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token'), placement: 'left')
%span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.") %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.")
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
%p %p
%strong Step 1. %strong Step 1.
Fetch and check out the branch for this merge request Fetch and check out the branch for this merge request
= clipboard_button(target: "pre#merge-info-1", title: "Copy commands to clipboard") = clipboard_button(target: "pre#merge-info-1", title: _("Copy commands"))
%pre.dark#merge-info-1 %pre.dark#merge-info-1
- if @merge_request.for_fork? - if @merge_request.for_fork?
:preserve :preserve
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
%p %p
%strong Step 3. %strong Step 3.
Merge the branch and fix any conflicts that come up Merge the branch and fix any conflicts that come up
= clipboard_button(target: "pre#merge-info-3", title: "Copy commands to clipboard") = clipboard_button(target: "pre#merge-info-3", title: _("Copy commands"))
%pre.dark#merge-info-3 %pre.dark#merge-info-3
- if @merge_request.for_fork? - if @merge_request.for_fork?
:preserve :preserve
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
%p %p
%strong Step 4. %strong Step 4.
Push the result of the merge to GitLab Push the result of the merge to GitLab
= clipboard_button(target: "pre#merge-info-4", title: "Copy commands to clipboard") = clipboard_button(target: "pre#merge-info-4", title: _("Copy commands"))
%pre.dark#merge-info-4 %pre.dark#merge-info-4
:preserve :preserve
git push origin "#{h @merge_request.target_branch}" git push origin "#{h @merge_request.target_branch}"
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= custom_icon('ellipsis_v') = custom_icon('ellipsis_v')
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
%li %li
= clipboard_button(text: noteable_note_url(note), title: 'Copy reference to clipboard', button_text: 'Copy link', class: 'btn-clipboard', hide_tooltip: true, hide_button_icon: true) = clipboard_button(text: noteable_note_url(note), title: _('Copy reference'), button_text: _('Copy link'), class: 'btn-clipboard', hide_tooltip: true, hide_button_icon: true)
- unless is_current_user - unless is_current_user
%li %li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
......
...@@ -58,4 +58,4 @@ ...@@ -58,4 +58,4 @@
= sprite_icon('ellipsis_h', size: 12) = sprite_icon('ellipsis_h', size: 12)
%span.js-details-content.hide %span.js-details-content.hide
= link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full" = link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full"
= clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard") = clipboard_button(text: @pipeline.sha, title: _("Copy commit SHA"))
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
%td %td
- if trigger.has_token_exposed? - if trigger.has_token_exposed?
%span= trigger.token %span= trigger.token
= clipboard_button(text: trigger.token, title: "Copy trigger token to clipboard") = clipboard_button(text: trigger.token, title: _("Copy trigger token"))
- else - else
%span= trigger.short_token %span= trigger.short_token
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' } = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
.input-group-append .input-group-append
= clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard") = clipboard_button(target: '#project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'shared/geo_modal_button' = render_if_exists 'shared/geo_modal_button'
......
- container_title = local_assigns.fetch(:container_title, _('Your New Personal Access Token')) - container_title = local_assigns.fetch(:container_title, _('Your New Personal Access Token'))
- clipboard_button_title = local_assigns.fetch(:clipboard_button_title, _('Copy personal access token to clipboard')) - clipboard_button_title = local_assigns.fetch(:clipboard_button_title, _('Copy personal access token'))
.created-personal-access-token-container .created-personal-access-token-container
%h5.prepend-top-0 %h5.prepend-top-0
......
...@@ -148,13 +148,13 @@ ...@@ -148,13 +148,13 @@
- project_ref = issuable_sidebar[:reference] - project_ref = issuable_sidebar[:reference]
.block.project-reference .block.project-reference
.sidebar-collapsed-icon.dont-change-state .sidebar-collapsed-icon.dont-change-state
= clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport') = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
.cross-project-reference.hide-collapsed .cross-project-reference.hide-collapsed
%span %span
= _('Reference:') = _('Reference:')
%cite{ title: project_ref } %cite{ title: project_ref }
= project_ref = project_ref
= clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport') = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport')
- if issuable_sidebar.dig(:current_user, :can_move) - if issuable_sidebar.dig(:current_user, :can_move)
.block.js-sidebar-move-issue-block .block.js-sidebar-move-issue-block
......
...@@ -142,10 +142,10 @@ ...@@ -142,10 +142,10 @@
- if milestone_ref.present? - if milestone_ref.present?
.block.reference .block.reference
.sidebar-collapsed-icon.dont-change-state .sidebar-collapsed-icon.dont-change-state
= clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left", boundary: 'viewport') = clipboard_button(text: milestone_ref, title: _("Copy reference"), placement: "left", boundary: 'viewport')
.cross-project-reference.hide-collapsed .cross-project-reference.hide-collapsed
%span %span
Reference: Reference:
%cite{ title: milestone_ref } %cite{ title: milestone_ref }
= milestone_ref = milestone_ref
= clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left", boundary: 'viewport') = clipboard_button(text: milestone_ref, title: _("Copy reference"), placement: "left", boundary: 'viewport')
...@@ -46,5 +46,5 @@ ...@@ -46,5 +46,5 @@
%strong.embed-toggle-list-item= _("Share") %strong.embed-toggle-list-item= _("Share")
%input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed } %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
.input-group-append .input-group-append
= clipboard_button(title: s_('Copy to clipboard'), class: 'js-clipboard-btn snippet-clipboard-btn btn btn-default', target: '.js-snippet-url-area') = clipboard_button(title: _('Copy'), class: 'js-clipboard-btn snippet-clipboard-btn btn btn-default', target: '.js-snippet-url-area')
.clearfix .clearfix
---
title: Feature enabling embedded audio elements in markdown.
merge_request: 17860
author: Jesse Hall @jessehall3
type: added
---
title: Add property to enable metrics dashboards to be rearranged
merge_request: 16605
author:
type: changed
---
title: HTML-escape search term in empty message
merge_request: 18319
author:
type: security
...@@ -125,4 +125,5 @@ class Rack::Attack ...@@ -125,4 +125,5 @@ class Rack::Attack
end end
end end
::Rack::Attack.extend_if_ee('::EE::Gitlab::Rack::Attack') # rubocop: disable Cop/InjectEnterpriseEditionModule
::Rack::Attack::Request.prepend_if_ee('::EE::Gitlab::Rack::Attack::Request') ::Rack::Attack::Request.prepend_if_ee('::EE::Gitlab::Rack::Attack::Request')
...@@ -30,7 +30,7 @@ en: ...@@ -30,7 +30,7 @@ en:
origin: Origin origin: Origin
line: line line: line
line_capitalized: Line line_capitalized: Line
copy_to_clipboard: Copy to clipboard copy_to_clipboard: Copy
query_plan: Query Plan query_plan: Query Plan
events: Events events: Events
percent: '%' percent: '%'
......
# frozen_string_literal: true
class AddIncidentManagementThrottleColumnsToApplicationSetting < ActiveRecord::Migration[5.2]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def up
add_column(:application_settings,
:throttle_incident_management_notification_enabled,
:boolean,
null: false,
default: false)
add_column(:application_settings,
:throttle_incident_management_notification_period_in_seconds,
:integer,
default: 3_600)
add_column(:application_settings,
:throttle_incident_management_notification_per_period,
:integer,
default: 3_600)
end
def down
remove_column :application_settings, :throttle_incident_management_notification_enabled
remove_column :application_settings, :throttle_incident_management_notification_period_in_seconds
remove_column :application_settings, :throttle_incident_management_notification_per_period
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_09_29_180827) do ActiveRecord::Schema.define(version: 2019_09_30_025655) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
...@@ -326,6 +326,9 @@ ActiveRecord::Schema.define(version: 2019_09_29_180827) do ...@@ -326,6 +326,9 @@ ActiveRecord::Schema.define(version: 2019_09_29_180827) do
t.integer "throttle_protected_paths_requests_per_period", default: 10, null: false t.integer "throttle_protected_paths_requests_per_period", default: 10, null: false
t.integer "throttle_protected_paths_period_in_seconds", default: 60, null: false t.integer "throttle_protected_paths_period_in_seconds", default: 60, null: false
t.string "protected_paths", limit: 255, default: ["/users/password", "/users/sign_in", "/api/v3/session.json", "/api/v3/session", "/api/v4/session.json", "/api/v4/session", "/users", "/users/confirmation", "/unsubscribes/", "/import/github/personal_access_token"], array: true t.string "protected_paths", limit: 255, default: ["/users/password", "/users/sign_in", "/api/v3/session.json", "/api/v3/session", "/api/v4/session.json", "/api/v4/session", "/users", "/users/confirmation", "/unsubscribes/", "/import/github/personal_access_token"], array: true
t.boolean "throttle_incident_management_notification_enabled", default: false, null: false
t.integer "throttle_incident_management_notification_period_in_seconds", default: 3600
t.integer "throttle_incident_management_notification_per_period", default: 3600
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id" t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id" t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id" t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
......
...@@ -429,6 +429,120 @@ args: { ...@@ -429,6 +429,120 @@ args: {
} }
``` ```
## Response signature validation (required)
We require Identity Providers to sign SAML responses to ensure that the assertions are
not tampered with.
This prevents user impersonation and prevents privilege escalation when specific group
membership is required. Typically this:
- Is configured using `idp_cert_fingerprint`.
- Includes the full certificate in the response, although if your Identity Provider
doesn't support this, you can directly configure GitLab using the `idp_cert` option.
Example configuration with `idp_cert_fingerprint`:
```yaml
args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
}
```
Example configuration with `idp_cert`:
```yaml
args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert: '-----BEGIN CERTIFICATE-----
<redacted>
-----END CERTIFICATE-----',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
}
```
If the response signature validation is configured incorrectly, you can see error messages
such as:
- A key validation error.
- Digest mismatch.
- Fingerprint mismatch.
Refer to the [troubleshooting section](#troubleshooting) for more information on
debugging these errors.
## Assertion Encryption (optional)
GitLab requires the use of TLS encryption with SAML, but in some cases there can be a
need for additional encryption of the assertions.
This may be the case, for example, if you terminate TLS encryption early at a load
balancer and include sensitive details in assertions that you do not want appearing
in logs. Most organizations should not need additional encryption at this layer.
The SAML integration supports EncryptedAssertion. You need to define the private key and the public certificate of your GitLab instance in the SAML settings:
```yaml
args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
certificate: '-----BEGIN CERTIFICATE-----
<redacted>
-----END CERTIFICATE-----',
private_key: '-----BEGIN PRIVATE KEY-----
<redacted>
-----END PRIVATE KEY-----'
}
```
Your Identity Provider will encrypt the assertion with the public certificate of GitLab. GitLab will decrypt the EncryptedAssertion with its private key.
NOTE: **Note:**
This integration uses the `certificate` and `private_key` settings for both assertion encryption and request signing.
## Request signing (optional)
Another optional configuration is to sign SAML authentication requests. GitLab SAML Requests uses the SAML redirect binding so this is not necessary, unlike the SAML POST binding where signing is required to prevent intermediaries tampering with the requests.
In order to sign, you need to create a private key and public certificate pair for your GitLab instance to use for SAML. The settings related to signing can be set in the `security` section of the configuration.
For example:
```yaml
args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
certificate: '-----BEGIN CERTIFICATE-----
<redacted>
-----END CERTIFICATE-----',
private_key: '-----BEGIN PRIVATE KEY-----
<redacted>
-----END PRIVATE KEY-----',
security: {
authn_requests_signed: true, # enable signature on AuthNRequest
want_assertions_signed: true, # enable the requirement of signed assertion
embed_sign: true, # embedded signature or HTTP GET parameter signature
metadata_signed: false, # enable signature on Metadata
signature_method: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
digest_method: 'http://www.w3.org/2001/04/xmlenc#sha256',
}
}
```
GitLab will sign the request with the provided private key. GitLab will include the configured public x500 certificate in the metadata for your Identity Provider to validate the signature of the received request with. For more information on this option, see the [ruby-saml gem documentation](https://github.com/onelogin/ruby-saml/tree/v1.7.0). The `ruby-saml` gem is used by the [omniauth-saml gem](https://github.com/omniauth/omniauth-saml) to implement the client side of the SAML authentication.
## Troubleshooting ## Troubleshooting
### 500 error after login ### 500 error after login
......
...@@ -11,12 +11,12 @@ to log the IP address of the user. ...@@ -11,12 +11,12 @@ to log the IP address of the user.
One way to mitigate this is by proxying any external images to a server you One way to mitigate this is by proxying any external images to a server you
control. control.
GitLab can be configured to use an asset proxy server when requesting external images/videos in GitLab can be configured to use an asset proxy server when requesting external images/videos/audio in
issues, comments, etc. This helps ensure that malicious images do not expose the user's IP address issues, comments, etc. This helps ensure that malicious images do not expose the user's IP address
when they are fetched. when they are fetched.
We currently recommend using [cactus/go-camo](https://github.com/cactus/go-camo#how-it-works) We currently recommend using [cactus/go-camo](https://github.com/cactus/go-camo#how-it-works)
as it supports proxying video and is more configurable. as it supports proxying video, audio, and is more configurable.
## Installing Camo server ## Installing Camo server
...@@ -52,7 +52,7 @@ To install a Camo server as an asset proxy: ...@@ -52,7 +52,7 @@ To install a Camo server as an asset proxy:
## Using the Camo server ## Using the Camo server
Once the Camo server is running and you've enabled the GitLab settings, any image or video that Once the Camo server is running and you've enabled the GitLab settings, any image, video, or audio that
references an external source will get proxied to the Camo server. references an external source will get proxied to the Camo server.
For example, the following is a link to an image in Markdown: For example, the following is a link to an image in Markdown:
......
...@@ -108,7 +108,7 @@ changing how standard markdown is used: ...@@ -108,7 +108,7 @@ changing how standard markdown is used:
| [code blocks](#code-spans-and-blocks) | [colored code and syntax highlighting](#colored-code-and-syntax-highlighting) | | [code blocks](#code-spans-and-blocks) | [colored code and syntax highlighting](#colored-code-and-syntax-highlighting) |
| [emphasis](#emphasis) | [multiple underscores in words](#multiple-underscores-in-words-and-mid-word-emphasis) | [emphasis](#emphasis) | [multiple underscores in words](#multiple-underscores-in-words-and-mid-word-emphasis)
| [headers](#headers) | [linkable Header IDs](#header-ids-and-links) | | [headers](#headers) | [linkable Header IDs](#header-ids-and-links) |
| [images](#images) | [embedded videos](#videos) | | [images](#images) | [embedded videos](#videos) and [audio](#audio) |
| [linebreaks](#line-breaks) | [more linebreak control](#newlines) | | [linebreaks](#line-breaks) | [more linebreak control](#newlines) |
| [links](#links) | [automatically linking URLs](#url-auto-linking) | | [links](#links) | [automatically linking URLs](#url-auto-linking) |
...@@ -899,6 +899,23 @@ Here's a sample video: ...@@ -899,6 +899,23 @@ Here's a sample video:
![Sample Video](img/markdown_video.mp4) ![Sample Video](img/markdown_video.mp4)
#### Audio
> If this is not rendered correctly, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#audio).
Similar to videos, link tags for files with an audio extension are automatically converted to
an audio player. The valid audio extensions are `.mp3`, `.ogg`, and `.wav`:
```md
Here's a sample audio clip:
![Sample Audio](img/markdown_audio.mp3)
```
Here's a sample audio clip:
![Sample Audio](img/markdown_audio.mp3)
### Inline HTML ### Inline HTML
> To see the markdown rendered within HTML in the second example, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#inline-html). > To see the markdown rendered within HTML in the second example, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#inline-html).
......
...@@ -15,6 +15,6 @@ below to create one: ...@@ -15,6 +15,6 @@ below to create one:
![Jira API token](img/jira_api_token.png) ![Jira API token](img/jira_api_token.png)
1. Click **Copy to clipboard**, or click **View** and write down the new API token. It is required when [configuring GitLab](jira.md#configuring-gitlab). 1. Click **Copy**, or click **View** and write down the new API token. It is required when [configuring GitLab](jira.md#configuring-gitlab).
The Jira configuration is complete. You need the newly created token, and the associated email address, when [configuring GitLab](jira.md#configuring-gitlab) in the next section. The Jira configuration is complete. You need the newly created token, and the associated email address, when [configuring GitLab](jira.md#configuring-gitlab) in the next section.
...@@ -141,9 +141,9 @@ for the issue. This will automatically enable if you participate in the issue in ...@@ -141,9 +141,9 @@ for the issue. This will automatically enable if you participate in the issue in
#### 14. Reference #### 14. Reference
- A quick "copy to clipboard" button for that issue's reference, which looks like `foo/bar#xxx`, - A quick "copy" button for that issue's reference, which looks like
where `foo` is the `username` or `groupname`, `bar` is the `project-name`, and `foo/bar#xxx`, where `foo` is the `username` or `groupname`, `bar` is the
`xxx` is the issue number. `project-name`, and `xxx` is the issue number.
#### 15. Edit #### 15. Edit
......
...@@ -52,7 +52,7 @@ Here's how the process would look like: ...@@ -52,7 +52,7 @@ Here's how the process would look like:
![Check out branch button](img/checkout_button.png) ![Check out branch button](img/checkout_button.png)
1. Use the copy to clipboard button to copy the first command and paste them 1. Use the copy button to copy the first command and paste them
in your terminal: in your terminal:
```sh ```sh
......
...@@ -70,8 +70,8 @@ To embed a snippet, first make sure that: ...@@ -70,8 +70,8 @@ To embed a snippet, first make sure that:
- In **Project > Settings > Permissions**, the snippets permissions are - In **Project > Settings > Permissions**, the snippets permissions are
set to **Everyone with access** set to **Everyone with access**
Once the above conditions are met, the "Embed" section will appear in your snippet Once the above conditions are met, the "Embed" section will appear in your
where you can simply click on the "Copy to clipboard" button. This copies a one-line snippet where you can simply click on the "Copy" button. This copies a one-line
script that you can add to any website or blog post. script that you can add to any website or blog post.
Here's how an example code looks like: Here's how an example code looks like:
......
# frozen_string_literal: true
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/audio.js
module Banzai
module Filter
# Find every image that isn't already wrapped in an `a` tag, and that has
# a `src` attribute ending with an audio extension, add a new audio node and
# a "Download" link in the case the audio cannot be played.
class AudioLinkFilter < HTML::Pipeline::Filter
def call
doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |el|
el.replace(audio_node(doc, el)) if has_audio_extension?(el)
end
doc
end
private
def has_audio_extension?(element)
src = element.attr('data-canonical-src').presence || element.attr('src')
return unless src.present?
src_ext = File.extname(src).sub('.', '').downcase
Gitlab::FileTypeDetection::SAFE_AUDIO_EXT.include?(src_ext)
end
def audio_node(doc, element)
container = doc.document.create_element(
'div',
class: 'audio-container'
)
audio = doc.document.create_element(
'audio',
src: element['src'],
controls: true,
'data-setup' => '{}',
'data-title' => element['title'] || element['alt'])
link = doc.document.create_element(
'a',
element['title'] || element['alt'],
href: element['src'],
target: '_blank',
rel: 'noopener noreferrer',
title: "Download '#{element['title'] || element['alt']}'")
# make sure the original non-proxied src carries over
if element['data-canonical-src']
audio['data-canonical-src'] = element['data-canonical-src']
link['data-canonical-src'] = element['data-canonical-src']
end
download_paragraph = doc.document.create_element('p')
download_paragraph.children = link
container.add_child(audio)
container.add_child(download_paragraph)
container
end
end
end
end
...@@ -65,7 +65,7 @@ module Banzai ...@@ -65,7 +65,7 @@ module Banzai
el.attribute('href') el.attribute('href')
end end
attrs += doc.search('img, video').flat_map do |el| attrs += doc.search('img, video, audio').flat_map do |el|
[el.attribute('src'), el.attribute('data-src')] [el.attribute('src'), el.attribute('data-src')]
end end
...@@ -83,7 +83,7 @@ module Banzai ...@@ -83,7 +83,7 @@ module Banzai
get_blob_types(paths).each do |name, type| get_blob_types(paths).each do |name, type|
if type == :blob if type == :blob
blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: name), project) blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: name), project)
uri_types[name] = blob.image? || blob.video? ? :raw : :blob uri_types[name] = blob.image? || blob.video? || blob.audio? ? :raw : :blob
else else
uri_types[name] = type uri_types[name] = type
end end
......
...@@ -15,7 +15,7 @@ module Banzai ...@@ -15,7 +15,7 @@ module Banzai
doc.search('a:not(.gfm)').each { |el| process_link(el.attribute('href'), el) } doc.search('a:not(.gfm)').each { |el| process_link(el.attribute('href'), el) }
doc.search('video').each { |el| process_link(el.attribute('src'), el) } doc.search('video, audio').each { |el| process_link(el.attribute('src'), el) }
doc.search('img').each do |el| doc.search('img').each do |el|
attr = el.attribute('data-src') || el.attribute('src') attr = el.attribute('data-src') || el.attribute('src')
......
...@@ -26,6 +26,7 @@ module Banzai ...@@ -26,6 +26,7 @@ module Banzai
Filter::ColorFilter, Filter::ColorFilter,
Filter::MermaidFilter, Filter::MermaidFilter,
Filter::VideoLinkFilter, Filter::VideoLinkFilter,
Filter::AudioLinkFilter,
Filter::ImageLazyLoadFilter, Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter, Filter::ImageLinkFilter,
Filter::InlineMetricsFilter, Filter::InlineMetricsFilter,
......
...@@ -1018,7 +1018,7 @@ into similar problems in the future (e.g. when new tables are created). ...@@ -1018,7 +1018,7 @@ into similar problems in the future (e.g. when new tables are created).
end end
model_class.each_batch(of: batch_size) do |relation, index| model_class.each_batch(of: batch_size) do |relation, index|
start_id, end_id = relation.pluck('MIN(id), MAX(id)').first start_id, end_id = relation.pluck(Arel.sql('MIN(id), MAX(id)')).first
# `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for # `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for
# the same time, which is not helpful in most cases where we wish to # the same time, which is not helpful in most cases where we wish to
......
...@@ -10,14 +10,14 @@ module Gitlab ...@@ -10,14 +10,14 @@ module Gitlab
return unless name = markdown_name return unless name = markdown_name
markdown = "[#{name.gsub(']', '\\]')}](#{secure_url})" markdown = "[#{name.gsub(']', '\\]')}](#{secure_url})"
markdown = "!#{markdown}" if image_or_video? || dangerous_image_or_video? markdown = "!#{markdown}" if embeddable? || dangerous_embeddable?
markdown markdown
end end
def markdown_name def markdown_name
return unless filename.present? return unless filename.present?
image_or_video? ? File.basename(filename, File.extname(filename)) : filename embeddable? ? File.basename(filename, File.extname(filename)) : filename
end end
end end
end end
...@@ -26,11 +26,13 @@ module Gitlab ...@@ -26,11 +26,13 @@ module Gitlab
# on IE >= 9. # on IE >= 9.
# http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
SAFE_VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze SAFE_VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze
SAFE_AUDIO_EXT = %w[mp3 oga ogg spx wav].freeze
# These extension types can contain dangerous code and should only be embedded inline with # These extension types can contain dangerous code and should only be embedded inline with
# proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline". # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
DANGEROUS_IMAGE_EXT = %w[svg].freeze DANGEROUS_IMAGE_EXT = %w[svg].freeze
DANGEROUS_VIDEO_EXT = [].freeze # None, yet DANGEROUS_VIDEO_EXT = [].freeze # None, yet
DANGEROUS_AUDIO_EXT = [].freeze # None, yet
def image? def image?
extension_match?(SAFE_IMAGE_EXT) extension_match?(SAFE_IMAGE_EXT)
...@@ -40,8 +42,12 @@ module Gitlab ...@@ -40,8 +42,12 @@ module Gitlab
extension_match?(SAFE_VIDEO_EXT) extension_match?(SAFE_VIDEO_EXT)
end end
def image_or_video? def audio?
image? || video? extension_match?(SAFE_AUDIO_EXT)
end
def embeddable?
image? || video? || audio?
end end
def dangerous_image? def dangerous_image?
...@@ -52,8 +58,12 @@ module Gitlab ...@@ -52,8 +58,12 @@ module Gitlab
extension_match?(DANGEROUS_VIDEO_EXT) extension_match?(DANGEROUS_VIDEO_EXT)
end end
def dangerous_image_or_video? def dangerous_audio?
dangerous_image? || dangerous_video? extension_match?(DANGEROUS_AUDIO_EXT)
end
def dangerous_embeddable?
dangerous_image? || dangerous_video? || dangerous_audio?
end end
private private
......
...@@ -37,8 +37,7 @@ module Gitlab ...@@ -37,8 +37,7 @@ module Gitlab
# - post_data: a string of raw POST data to use. Changes the HTTP verb to # - post_data: a string of raw POST data to use. Changes the HTTP verb to
# POST. # POST.
# #
# - user: a user to authenticate as. Only works if the user has a valid # - user: a user to authenticate as.
# personal access token.
# #
# - private_token: instead of providing a user instance, the token can be # - private_token: instead of providing a user instance, the token can be
# given as a string. Takes precedence over the user option. # given as a string. Takes precedence over the user option.
......
...@@ -1889,6 +1889,9 @@ msgstr "" ...@@ -1889,6 +1889,9 @@ msgstr ""
msgid "Are you sure? This will invalidate your registered applications and U2F devices." msgid "Are you sure? This will invalidate your registered applications and U2F devices."
msgstr "" msgstr ""
msgid "Arrange charts"
msgstr ""
msgid "Artifact ID" msgid "Artifact ID"
msgstr "" msgstr ""
...@@ -3399,13 +3402,13 @@ msgstr "" ...@@ -3399,13 +3402,13 @@ msgstr ""
msgid "ClusterIntegration|Copy CA Certificate" msgid "ClusterIntegration|Copy CA Certificate"
msgstr "" msgstr ""
msgid "ClusterIntegration|Copy Ingress Endpoint to clipboard" msgid "ClusterIntegration|Copy Ingress Endpoint"
msgstr "" msgstr ""
msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard" msgid "ClusterIntegration|Copy Jupyter Hostname"
msgstr "" msgstr ""
msgid "ClusterIntegration|Copy Knative Endpoint to clipboard" msgid "ClusterIntegration|Copy Knative Endpoint"
msgstr "" msgstr ""
msgid "ClusterIntegration|Copy Kubernetes cluster name" msgid "ClusterIntegration|Copy Kubernetes cluster name"
...@@ -4127,6 +4130,9 @@ msgstr "" ...@@ -4127,6 +4130,9 @@ msgstr ""
msgid "Configure limits for web and API requests." msgid "Configure limits for web and API requests."
msgstr "" msgstr ""
msgid "Configure limits on the number of inbound alerts able to be sent to a project."
msgstr ""
msgid "Configure paths to be protected by Rack Attack. A web server restart is required after changing these settings." msgid "Configure paths to be protected by Rack Attack. A web server restart is required after changing these settings."
msgstr "" msgstr ""
...@@ -4196,10 +4202,10 @@ msgstr "" ...@@ -4196,10 +4202,10 @@ msgstr ""
msgid "ContainerRegistry|Container Registry" msgid "ContainerRegistry|Container Registry"
msgstr "" msgstr ""
msgid "ContainerRegistry|Copy build command to clipboard" msgid "ContainerRegistry|Copy build command"
msgstr "" msgstr ""
msgid "ContainerRegistry|Copy push command to clipboard" msgid "ContainerRegistry|Copy push command"
msgstr "" msgstr ""
msgid "ContainerRegistry|Docker connection error" msgid "ContainerRegistry|Docker connection error"
...@@ -4330,16 +4336,19 @@ msgstr "" ...@@ -4330,16 +4336,19 @@ msgstr ""
msgid "Copied labels and milestone from %{source_issuable_reference}." msgid "Copied labels and milestone from %{source_issuable_reference}."
msgstr "" msgstr ""
msgid "Copy"
msgstr ""
msgid "Copy %{http_label} clone URL" msgid "Copy %{http_label} clone URL"
msgstr "" msgstr ""
msgid "Copy %{protocol} clone URL" msgid "Copy %{protocol} clone URL"
msgstr "" msgstr ""
msgid "Copy %{proxy_url} to clipboard" msgid "Copy %{proxy_url}"
msgstr "" msgstr ""
msgid "Copy ID to clipboard" msgid "Copy ID"
msgstr "" msgstr ""
msgid "Copy KRB5 clone URL" msgid "Copy KRB5 clone URL"
...@@ -4351,19 +4360,28 @@ msgstr "" ...@@ -4351,19 +4360,28 @@ msgstr ""
msgid "Copy SSH public key" msgid "Copy SSH public key"
msgstr "" msgstr ""
msgid "Copy URL to clipboard" msgid "Copy URL"
msgstr ""
msgid "Copy branch name"
msgstr ""
msgid "Copy command"
msgstr "" msgstr ""
msgid "Copy branch name to clipboard" msgid "Copy commands"
msgstr "" msgstr ""
msgid "Copy command to clipboard" msgid "Copy commit SHA"
msgstr "" msgstr ""
msgid "Copy commit SHA to clipboard" msgid "Copy file contents"
msgstr "" msgstr ""
msgid "Copy file path to clipboard" msgid "Copy file path"
msgstr ""
msgid "Copy impersonation token"
msgstr "" msgstr ""
msgid "Copy labels and milestone from %{source_issuable_reference}." msgid "Copy labels and milestone from %{source_issuable_reference}."
...@@ -4375,22 +4393,19 @@ msgstr "" ...@@ -4375,22 +4393,19 @@ msgstr ""
msgid "Copy link" msgid "Copy link"
msgstr "" msgstr ""
msgid "Copy personal access token to clipboard" msgid "Copy personal access token"
msgstr "" msgstr ""
msgid "Copy reference to clipboard" msgid "Copy reference"
msgstr "" msgstr ""
msgid "Copy secret to clipboard" msgid "Copy secret"
msgstr "" msgstr ""
msgid "Copy source to clipboard" msgid "Copy token"
msgstr "" msgstr ""
msgid "Copy to clipboard" msgid "Copy trigger token"
msgstr ""
msgid "Copy token to clipboard"
msgstr "" msgstr ""
msgid "Could not add admins as members" msgid "Could not add admins as members"
...@@ -5166,10 +5181,10 @@ msgstr "" ...@@ -5166,10 +5181,10 @@ msgstr ""
msgid "DeployTokens|Allows read-only access to the repository" msgid "DeployTokens|Allows read-only access to the repository"
msgstr "" msgstr ""
msgid "DeployTokens|Copy deploy token to clipboard" msgid "DeployTokens|Copy deploy token"
msgstr "" msgstr ""
msgid "DeployTokens|Copy username to clipboard" msgid "DeployTokens|Copy username"
msgstr "" msgstr ""
msgid "DeployTokens|Create deploy token" msgid "DeployTokens|Create deploy token"
...@@ -5736,6 +5751,9 @@ msgstr "" ...@@ -5736,6 +5751,9 @@ msgstr ""
msgid "Enable HTML emails" msgid "Enable HTML emails"
msgstr "" msgstr ""
msgid "Enable Incident Management inbound alert limit"
msgstr ""
msgid "Enable Pseudonymizer data collection" msgid "Enable Pseudonymizer data collection"
msgstr "" msgstr ""
...@@ -8240,6 +8258,9 @@ msgstr "" ...@@ -8240,6 +8258,9 @@ msgstr ""
msgid "Helps prevent bots from creating accounts." msgid "Helps prevent bots from creating accounts."
msgstr "" msgstr ""
msgid "Helps reduce alert volume (e.g. if creating too many issues)"
msgstr ""
msgid "Helps reduce request volume for protected paths" msgid "Helps reduce request volume for protected paths"
msgstr "" msgstr ""
...@@ -8584,6 +8605,9 @@ msgstr "" ...@@ -8584,6 +8605,9 @@ msgstr ""
msgid "In the next step, you'll be able to select the projects you want to import." msgid "In the next step, you'll be able to select the projects you want to import."
msgstr "" msgstr ""
msgid "Incident Management Limits"
msgstr ""
msgid "Incidents" msgid "Incidents"
msgstr "" msgstr ""
...@@ -9464,7 +9488,7 @@ msgid_plural "Limited to showing %d events at most" ...@@ -9464,7 +9488,7 @@ msgid_plural "Limited to showing %d events at most"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Link copied to clipboard" msgid "Link copied"
msgstr "" msgstr ""
msgid "Linked emails (%{email_count})" msgid "Linked emails (%{email_count})"
...@@ -14384,7 +14408,7 @@ msgstr "" ...@@ -14384,7 +14408,7 @@ msgstr ""
msgid "ServerlessDetails|pods in use" msgid "ServerlessDetails|pods in use"
msgstr "" msgstr ""
msgid "ServerlessURL|Copy URL to clipboard" msgid "ServerlessURL|Copy URL"
msgstr "" msgstr ""
msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster." msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster."
...@@ -15565,7 +15589,7 @@ msgstr "" ...@@ -15565,7 +15589,7 @@ msgstr ""
msgid "Switch to GitLab Next" msgid "Switch to GitLab Next"
msgstr "" msgstr ""
msgid "Switch to the source to copy it to the clipboard" msgid "Switch to the source to copy the file contents"
msgstr "" msgstr ""
msgid "Sync information" msgid "Sync information"
...@@ -17953,10 +17977,10 @@ msgstr "" ...@@ -17953,10 +17977,10 @@ msgstr ""
msgid "VisualReviewApp|%{stepStart}Step 5%{stepEnd}. Leave feedback in the Review App." msgid "VisualReviewApp|%{stepStart}Step 5%{stepEnd}. Leave feedback in the Review App."
msgstr "" msgstr ""
msgid "VisualReviewApp|Copy merge request ID to clipboard" msgid "VisualReviewApp|Copy merge request ID"
msgstr "" msgstr ""
msgid "VisualReviewApp|Copy script to clipboard" msgid "VisualReviewApp|Copy script"
msgstr "" msgstr ""
msgid "VisualReviewApp|Enable Visual Reviews" msgid "VisualReviewApp|Enable Visual Reviews"
...@@ -18022,7 +18046,7 @@ msgstr "" ...@@ -18022,7 +18046,7 @@ msgstr ""
msgid "Vulnerability|Severity" msgid "Vulnerability|Severity"
msgstr "" msgstr ""
msgid "Wait for the source to load to copy it to the clipboard" msgid "Wait for the file to load to copy its contents"
msgstr "" msgstr ""
msgid "Waiting for performance data" msgid "Waiting for performance data"
......
...@@ -178,6 +178,12 @@ describe 'Copy as GFM', :js do ...@@ -178,6 +178,12 @@ describe 'Copy as GFM', :js do
'![Video](https://example.com/video.mp4)' '![Video](https://example.com/video.mp4)'
) )
verify(
'AudioLinkFilter',
'![Audio](https://example.com/audio.wav)'
)
verify( verify(
'MathFilter: math as converted from GFM to HTML', 'MathFilter: math as converted from GFM to HTML',
......
...@@ -320,6 +320,10 @@ describe 'GitLab Markdown', :aggregate_failures do ...@@ -320,6 +320,10 @@ describe 'GitLab Markdown', :aggregate_failures do
expect(doc).to parse_video_links expect(doc).to parse_video_links
end end
aggregate_failures 'AudioLinkFilter' do
expect(doc).to parse_audio_links
end
aggregate_failures 'ColorFilter' do aggregate_failures 'ColorFilter' do
expect(doc).to parse_colors expect(doc).to parse_colors
end end
......
...@@ -101,7 +101,7 @@ describe 'Branches' do ...@@ -101,7 +101,7 @@ describe 'Branches' do
visit project_branches_filtered_path(project, state: 'all') visit project_branches_filtered_path(project, state: 'all')
expect(all('.all-branches').last).to have_selector('li', count: 20) expect(all('.all-branches').last).to have_selector('li', count: 20)
accept_confirm { find('.js-branch-add-pdf-text-binary .btn-remove').click } accept_confirm { first('.js-branch-item .btn-remove').click }
expect(all('.all-branches').last).to have_selector('li', count: 19) expect(all('.all-branches').last).to have_selector('li', count: 19)
end end
......
...@@ -286,6 +286,10 @@ However the wrapping tags cannot be mixed as such: ...@@ -286,6 +286,10 @@ However the wrapping tags cannot be mixed as such:
![My Video](/assets/videos/gitlab-demo.mp4) ![My Video](/assets/videos/gitlab-demo.mp4)
### Audio
![My Audio Clip](/assets/audio/gitlab-demo.wav)
### Colors ### Colors
`#F00` `#F00`
......
...@@ -90,7 +90,7 @@ exports[`Repository last commit component renders commit widget 1`] = ` ...@@ -90,7 +90,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
<clipboardbutton-stub <clipboardbutton-stub
cssclass="btn-default" cssclass="btn-default"
text="123456789" text="123456789"
title="Copy commit SHA to clipboard" title="Copy commit SHA"
tooltipplacement="bottom" tooltipplacement="bottom"
/> />
</div> </div>
...@@ -193,7 +193,7 @@ exports[`Repository last commit component renders the signature HTML as returned ...@@ -193,7 +193,7 @@ exports[`Repository last commit component renders the signature HTML as returned
<clipboardbutton-stub <clipboardbutton-stub
cssclass="btn-default" cssclass="btn-default"
text="123456789" text="123456789"
title="Copy commit SHA to clipboard" title="Copy commit SHA"
tooltipplacement="bottom" tooltipplacement="bottom"
/> />
</div> </div>
......
...@@ -14,7 +14,7 @@ describe('modal copy button', () => { ...@@ -14,7 +14,7 @@ describe('modal copy button', () => {
wrapper = shallowMount(Component, { wrapper = shallowMount(Component, {
propsData: { propsData: {
text: 'copy me', text: 'copy me',
title: 'Copy this value into Clipboard!', title: 'Copy this value',
}, },
}); });
}); });
......
...@@ -166,7 +166,7 @@ describe ButtonHelper do ...@@ -166,7 +166,7 @@ describe ButtonHelper do
it 'shows copy to clipboard button with default configuration and no text set to copy' do it 'shows copy to clipboard button with default configuration and no text set to copy' do
expect(element.attr('class')).to eq('btn btn-clipboard btn-transparent') expect(element.attr('class')).to eq('btn btn-clipboard btn-transparent')
expect(element.attr('type')).to eq('button') expect(element.attr('type')).to eq('button')
expect(element.attr('aria-label')).to eq('Copy to clipboard') expect(element.attr('aria-label')).to eq('Copy')
expect(element.attr('data-toggle')).to eq('tooltip') expect(element.attr('data-toggle')).to eq('tooltip')
expect(element.attr('data-placement')).to eq('bottom') expect(element.attr('data-placement')).to eq('bottom')
expect(element.attr('data-container')).to eq('body') expect(element.attr('data-container')).to eq('body')
......
...@@ -142,9 +142,9 @@ describe SearchHelper do ...@@ -142,9 +142,9 @@ describe SearchHelper do
describe 'search_entries_empty_message' do describe 'search_entries_empty_message' do
it 'returns the formatted entry message' do it 'returns the formatted entry message' do
message = search_entries_empty_message('projects', 'foo') message = search_entries_empty_message('projects', '<h1>foo</h1>')
expect(message).to eq("We couldn't find any projects matching <code>foo</code>") expect(message).to eq("We couldn't find any projects matching <code>&lt;h1&gt;foo&lt;/h1&gt;</code>")
expect(message).to be_html_safe expect(message).to be_html_safe
end end
end end
......
...@@ -101,7 +101,7 @@ describe('Blob viewer', () => { ...@@ -101,7 +101,7 @@ describe('Blob viewer', () => {
it('has tooltip when disabled', () => { it('has tooltip when disabled', () => {
expect(copyButton.getAttribute('data-original-title')).toBe( expect(copyButton.getAttribute('data-original-title')).toBe(
'Switch to the source to copy it to the clipboard', 'Switch to the source to copy the file contents',
); );
}); });
...@@ -136,7 +136,7 @@ describe('Blob viewer', () => { ...@@ -136,7 +136,7 @@ describe('Blob viewer', () => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click(); document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(() => { setTimeout(() => {
expect(copyButton.getAttribute('data-original-title')).toBe('Copy source to clipboard'); expect(copyButton.getAttribute('data-original-title')).toBe('Copy file contents');
done(); done();
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlToast } from '@gitlab/ui'; import { GlToast } from '@gitlab/ui';
import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue'; import Dashboard from '~/monitoring/components/dashboard.vue';
import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants'; import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
...@@ -51,11 +52,6 @@ describe('Dashboard', () => { ...@@ -51,11 +52,6 @@ describe('Dashboard', () => {
<div class="layout-page"></div> <div class="layout-page"></div>
`); `);
window.gon = {
...window.gon,
ee: false,
};
store = createStore(); store = createStore();
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
DashboardComponent = Vue.extend(Dashboard); DashboardComponent = Vue.extend(Dashboard);
...@@ -378,7 +374,101 @@ describe('Dashboard', () => { ...@@ -378,7 +374,101 @@ describe('Dashboard', () => {
}); });
}); });
// https://gitlab.com/gitlab-org/gitlab-foss/issues/66922 describe('drag and drop function', () => {
let wrapper;
let expectedPanelCount; // also called metrics, naming to be improved: https://gitlab.com/gitlab-org/gitlab/issues/31565
const findDraggables = () => wrapper.findAll(VueDraggable);
const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled'));
const findDraggablePanels = () => wrapper.findAll('.js-draggable-panel');
const findRearrangeButton = () => wrapper.find('.js-rearrange-button');
beforeEach(done => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
expectedPanelCount = metricsGroupsAPIResponse.data.reduce(
(acc, d) => d.metrics.length + acc,
0,
);
store.dispatch('monitoringDashboard/setFeatureFlags', { additionalPanelTypesEnabled: true });
wrapper = shallowMount(DashboardComponent, {
localVue,
sync: false,
propsData: { ...propsData, hasMetrics: true },
store,
});
// not using $nextTicket becuase we must wait for the dashboard
// to be populated with the mock data results.
setTimeout(done);
});
it('wraps vuedraggable', () => {
expect(findDraggablePanels().exists()).toBe(true);
expect(findDraggablePanels().length).toEqual(expectedPanelCount);
});
it('is disabled by default', () => {
expect(findRearrangeButton().exists()).toBe(false);
expect(findEnabledDraggables().length).toBe(0);
});
describe('when rearrange is enabled', () => {
beforeEach(done => {
wrapper.setProps({ rearrangePanelsAvailable: true });
wrapper.vm.$nextTick(done);
});
it('displays rearrange button', () => {
expect(findRearrangeButton().exists()).toBe(true);
});
describe('when rearrange button is clicked', () => {
const findFirstDraggableRemoveButton = () =>
findDraggablePanels()
.at(0)
.find('.js-draggable-remove');
beforeEach(done => {
findRearrangeButton().vm.$emit('click');
wrapper.vm.$nextTick(done);
});
it('it enables draggables', () => {
expect(findRearrangeButton().attributes('pressed')).toBeTruthy();
expect(findEnabledDraggables()).toEqual(findDraggables());
});
it('shows a remove button, which removes a panel', done => {
expect(findFirstDraggableRemoveButton().isEmpty()).toBe(false);
expect(findDraggablePanels().length).toEqual(expectedPanelCount);
findFirstDraggableRemoveButton().trigger('click');
wrapper.vm.$nextTick(() => {
// At present graphs will not be removed in backend
// See https://gitlab.com/gitlab-org/gitlab/issues/27835
expect(findDraggablePanels().length).toEqual(expectedPanelCount - 1);
done();
});
});
it('it disables draggables when clicked again', done => {
findRearrangeButton().vm.$emit('click');
wrapper.vm.$nextTick(() => {
expect(findRearrangeButton().attributes('pressed')).toBeFalsy();
expect(findEnabledDraggables().length).toBe(0);
done();
});
});
});
});
afterEach(() => {
wrapper.destroy();
});
});
// https://gitlab.com/gitlab-org/gitlab-ce/issues/66922
// eslint-disable-next-line jasmine/no-disabled-tests // eslint-disable-next-line jasmine/no-disabled-tests
xdescribe('link to chart', () => { xdescribe('link to chart', () => {
let wrapper; let wrapper;
......
...@@ -14,7 +14,7 @@ describe('clipboard button', () => { ...@@ -14,7 +14,7 @@ describe('clipboard button', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
text: 'copy me', text: 'copy me',
title: 'Copy this value into Clipboard!', title: 'Copy this value',
cssClass: 'btn-danger', cssClass: 'btn-danger',
}); });
}); });
...@@ -26,7 +26,7 @@ describe('clipboard button', () => { ...@@ -26,7 +26,7 @@ describe('clipboard button', () => {
}); });
it('should have a tooltip with default values', () => { it('should have a tooltip with default values', () => {
expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value into Clipboard!'); expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value');
}); });
it('should render provided classname', () => { it('should render provided classname', () => {
...@@ -39,7 +39,7 @@ describe('clipboard button', () => { ...@@ -39,7 +39,7 @@ describe('clipboard button', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
text: 'copy me', text: 'copy me',
gfm: '`path/to/file`', gfm: '`path/to/file`',
title: 'Copy this value into Clipboard!', title: 'Copy this value',
cssClass: 'btn-danger', cssClass: 'btn-danger',
}); });
......
# frozen_string_literal: true
require 'spec_helper'
describe Banzai::Filter::AudioLinkFilter do
def filter(doc, contexts = {})
contexts.reverse_merge!({
project: project
})
described_class.call(doc, contexts)
end
def link_to_image(path)
return '<img/>' if path.nil?
%(<img src="#{path}"/>)
end
let(:project) { create(:project, :repository) }
shared_examples 'an audio element' do
let(:image) { link_to_image(src) }
it 'replaces the image tag with an audio tag' do
container = filter(image).children.first
expect(container.name).to eq 'div'
expect(container['class']).to eq 'audio-container'
audio, paragraph = container.children
expect(audio.name).to eq 'audio'
expect(audio['src']).to eq src
expect(paragraph.name).to eq 'p'
link = paragraph.children.first
expect(link.name).to eq 'a'
expect(link['href']).to eq src
expect(link['target']).to eq '_blank'
end
end
shared_examples 'an unchanged element' do |ext|
it 'leaves the document unchanged' do
element = filter(link_to_image(src)).children.first
expect(element.name).to eq 'img'
expect(element['src']).to eq src
end
end
context 'when the element src has an audio extension' do
Gitlab::FileTypeDetection::SAFE_AUDIO_EXT.each do |ext|
it_behaves_like 'an audio element' do
let(:src) { "/path/audio.#{ext}" }
end
it_behaves_like 'an audio element' do
let(:src) { "/path/audio.#{ext.upcase}" }
end
end
end
context 'when the element has no src attribute' do
let(:src) { nil }
it_behaves_like 'an unchanged element'
end
context 'when the element src is an image' do
let(:src) { '/path/my_image.jpg' }
it_behaves_like 'an unchanged element'
end
context 'when the element src has an invalid file extension' do
let(:src) { '/path/my_audio.somewav' }
it_behaves_like 'an unchanged element'
end
context 'when data-canonical-src is empty' do
let(:image) { %(<img src="#{src}" data-canonical-src=""/>) }
context 'and src is audio' do
let(:src) { '/path/audio.wav' }
it_behaves_like 'an audio element'
end
context 'and src is an image' do
let(:src) { '/path/my_image.jpg' }
it_behaves_like 'an unchanged element'
end
end
context 'when data-canonical-src is set' do
it 'uses the correct src' do
proxy_src = 'https://assets.example.com/6d8b63'
canonical_src = 'http://example.com/test.wav'
image = %(<img src="#{proxy_src}" data-canonical-src="#{canonical_src}"/>)
container = filter(image).children.first
expect(container['class']).to eq 'audio-container'
audio, paragraph = container.children
expect(audio['src']).to eq proxy_src
expect(audio['data-canonical-src']).to eq canonical_src
link = paragraph.children.first
expect(link['href']).to eq proxy_src
end
end
end
...@@ -29,6 +29,10 @@ describe Banzai::Filter::RelativeLinkFilter do ...@@ -29,6 +29,10 @@ describe Banzai::Filter::RelativeLinkFilter do
%(<video src="#{path}"></video>) %(<video src="#{path}"></video>)
end end
def audio(path)
%(<audio src="#{path}"></audio>)
end
def link(path) def link(path)
%(<a href="#{path}">#{path}</a>) %(<a href="#{path}">#{path}</a>)
end end
...@@ -82,6 +86,12 @@ describe Banzai::Filter::RelativeLinkFilter do ...@@ -82,6 +86,12 @@ describe Banzai::Filter::RelativeLinkFilter do
expect(doc.at_css('video')['src']).to eq 'files/videos/intro.mp4' expect(doc.at_css('video')['src']).to eq 'files/videos/intro.mp4'
end end
it 'does not modify any relative URL in audio' do
doc = filter(audio('files/audio/sample.wav'), commit: project.commit('audio'), ref: 'audio')
expect(doc.at_css('audio')['src']).to eq 'files/audio/sample.wav'
end
end end
context 'with a project_wiki' do context 'with a project_wiki' do
...@@ -218,6 +228,13 @@ describe Banzai::Filter::RelativeLinkFilter do ...@@ -218,6 +228,13 @@ describe Banzai::Filter::RelativeLinkFilter do
.to eq "/#{project_path}/raw/video/files/videos/intro.mp4" .to eq "/#{project_path}/raw/video/files/videos/intro.mp4"
end end
it 'rebuilds relative URL for audio in the repo' do
doc = filter(audio('files/audio/sample.wav'), commit: project.commit('audio'), ref: 'audio')
expect(doc.at_css('audio')['src'])
.to eq "/#{project_path}/raw/audio/files/audio/sample.wav"
end
it 'does not modify relative URL with an anchor only' do it 'does not modify relative URL with an anchor only' do
doc = filter(link('#section-1')) doc = filter(link('#section-1'))
expect(doc.at_css('a')['href']).to eq '#section-1' expect(doc.at_css('a')['href']).to eq '#section-1'
......
...@@ -60,6 +60,14 @@ describe Banzai::Filter::WikiLinkFilter do ...@@ -60,6 +60,14 @@ describe Banzai::Filter::WikiLinkFilter do
expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.mp4") expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.mp4")
end end
end end
context 'with "audio" html tag' do
it 'rewrites links' do
filtered_link = filter("<audio src='#{repository_upload_folder}/a.wav'></audio>", project_wiki: wiki).children[0]
expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.wav")
end
end
end end
describe "invalid links" do describe "invalid links" do
......
...@@ -260,11 +260,11 @@ describe Banzai::Pipeline::WikiPipeline do ...@@ -260,11 +260,11 @@ describe Banzai::Pipeline::WikiPipeline do
end end
end end
describe 'videos' do describe 'videos and audio' do
let(:namespace) { create(:namespace, name: "wiki_link_ns") } let_it_be(:namespace) { create(:namespace, name: "wiki_link_ns") }
let(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) } let_it_be(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) }
let(:project_wiki) { ProjectWiki.new(project, double(:user)) } let_it_be(:project_wiki) { ProjectWiki.new(project, double(:user)) }
let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) } let_it_be(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
it 'generates video html structure' do it 'generates video html structure' do
markdown = "![video_file](video_file_name.mp4)" markdown = "![video_file](video_file_name.mp4)"
...@@ -279,5 +279,19 @@ describe Banzai::Pipeline::WikiPipeline do ...@@ -279,5 +279,19 @@ describe Banzai::Pipeline::WikiPipeline do
expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video%20file%20name.mp4"') expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video%20file%20name.mp4"')
end end
it 'generates audio html structure' do
markdown = "![audio_file](audio_file_name.wav)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/audio_file_name.wav"')
end
it 'rewrites and replaces audio links names with white spaces to %20' do
markdown = "![audio file](audio file name.wav)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/audio%20file%20name.wav"')
end
end end
end end
...@@ -27,19 +27,35 @@ describe Gitlab::FileMarkdownLinkBuilder do ...@@ -27,19 +27,35 @@ describe Gitlab::FileMarkdownLinkBuilder do
end end
end end
context 'when file is an image or video' do context 'when file is an image' do
let(:filename) { 'dk.png' } let(:filename) { 'my_image.png' }
it 'returns preview markdown link' do it 'returns preview markdown link' do
expect(custom_class.markdown_link).to eq '![dk](/uploads/dk.png)' expect(custom_class.markdown_link).to eq '![my_image](/uploads/my_image.png)'
end end
end end
context 'when file is not an image or video' do context 'when file is video' do
let(:filename) { 'dk.zip' } let(:filename) { 'my_video.mp4' }
it 'returns preview markdown link' do
expect(custom_class.markdown_link).to eq '![my_video](/uploads/my_video.mp4)'
end
end
context 'when file is audio' do
let(:filename) { 'my_audio.wav' }
it 'returns preview markdown link' do
expect(custom_class.markdown_link).to eq '![my_audio](/uploads/my_audio.wav)'
end
end
context 'when file is not embeddable' do
let(:filename) { 'my_zip.zip' }
it 'returns markdown link' do it 'returns markdown link' do
expect(custom_class.markdown_link).to eq '[dk.zip](/uploads/dk.zip)' expect(custom_class.markdown_link).to eq '[my_zip.zip](/uploads/my_zip.zip)'
end end
end end
...@@ -53,19 +69,35 @@ describe Gitlab::FileMarkdownLinkBuilder do ...@@ -53,19 +69,35 @@ describe Gitlab::FileMarkdownLinkBuilder do
end end
describe 'mardown_name' do describe 'mardown_name' do
context 'when file is an image or video' do context 'when file is an image' do
let(:filename) { 'dk.png' } let(:filename) { 'my_image.png' }
it 'retrieves the name without the extension' do
expect(custom_class.markdown_name).to eq 'my_image'
end
end
context 'when file is video' do
let(:filename) { 'my_video.mp4' }
it 'retrieves the name without the extension' do
expect(custom_class.markdown_name).to eq 'my_video'
end
end
context 'when file is audio' do
let(:filename) { 'my_audio.wav' }
it 'retrieves the name without the extension' do it 'retrieves the name without the extension' do
expect(custom_class.markdown_name).to eq 'dk' expect(custom_class.markdown_name).to eq 'my_audio'
end end
end end
context 'when file is not an image or video' do context 'when file is not embeddable' do
let(:filename) { 'dk.zip' } let(:filename) { 'my_zip.zip' }
it 'retrieves the name with the extesion' do it 'retrieves the name with the extesion' do
expect(custom_class.markdown_name).to eq 'dk.zip' expect(custom_class.markdown_name).to eq 'my_zip.zip'
end end
end end
......
This diff is collapsed.
...@@ -43,6 +43,11 @@ describe Gitlab::Utils::SanitizeNodeLink do ...@@ -43,6 +43,11 @@ describe Gitlab::Utils::SanitizeNodeLink do
doc: HTML::Pipeline.parse("<video><source src='#{scheme}alert(1);'></video>"), doc: HTML::Pipeline.parse("<video><source src='#{scheme}alert(1);'></video>"),
attr: "src", attr: "src",
node_to_check: -> (doc) { doc.children.first.children.filter("source").first } node_to_check: -> (doc) { doc.children.first.children.filter("source").first }
},
audio: {
doc: HTML::Pipeline.parse("<audio><source src='#{scheme}alert(1);'></audio>"),
attr: "src",
node_to_check: -> (doc) { doc.children.first.children.filter("source").first }
} }
} }
......
...@@ -503,6 +503,8 @@ eos ...@@ -503,6 +503,8 @@ eos
expect(commit.uri_type('files/html')).to be(:tree) expect(commit.uri_type('files/html')).to be(:tree)
expect(commit.uri_type('files/images/logo-black.png')).to be(:raw) expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
expect(commit.uri_type('files/images/wm.svg')).to be(:raw) expect(commit.uri_type('files/images/wm.svg')).to be(:raw)
expect(project.commit('audio').uri_type('files/audio/clip.mp3')).to be(:raw)
expect(project.commit('audio').uri_type('files/audio/sample.wav')).to be(:raw)
expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw) expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw)
expect(commit.uri_type('files/js/application.js')).to be(:blob) expect(commit.uri_type('files/js/application.js')).to be(:blob)
end end
......
require 'spec_helper' require 'spec_helper'
describe 'Rack Attack global throttles' do describe 'Rack Attack global throttles' do
include RackAttackSpecHelpers
let(:settings) { Gitlab::CurrentSettings.current_application_settings } let(:settings) { Gitlab::CurrentSettings.current_application_settings }
# Start with really high limits and override them with low limits to ensure # Start with really high limits and override them with low limits to ensure
...@@ -22,15 +24,7 @@ describe 'Rack Attack global throttles' do ...@@ -22,15 +24,7 @@ describe 'Rack Attack global throttles' do
let(:period_in_seconds) { 10000 } let(:period_in_seconds) { 10000 }
let(:period) { period_in_seconds.seconds } let(:period) { period_in_seconds.seconds }
around do |example| include_context 'rack attack cache store'
# Instead of test environment's :null_store so the throttles can increment
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
# Make time-dependent tests deterministic
Timecop.freeze { example.run }
Rack::Attack.cache.store = Rails.cache
end
describe 'unauthenticated requests' do describe 'unauthenticated requests' do
let(:url_that_does_not_require_authentication) { '/users/sign_in' } let(:url_that_does_not_require_authentication) { '/users/sign_in' }
...@@ -361,30 +355,4 @@ describe 'Rack Attack global throttles' do ...@@ -361,30 +355,4 @@ describe 'Rack Attack global throttles' do
end end
end end
end end
def api_get_args_with_token_headers(partial_url, token_headers)
["/api/#{API::API.version}#{partial_url}", params: nil, headers: token_headers]
end
def rss_url(user)
"/dashboard/projects.atom?feed_token=#{user.feed_token}"
end
def private_token_headers(user)
{ 'HTTP_PRIVATE_TOKEN' => user.private_token }
end
def personal_access_token_headers(personal_access_token)
{ 'HTTP_PRIVATE_TOKEN' => personal_access_token.token }
end
def oauth_token_headers(oauth_access_token)
{ 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" }
end
def expect_rejection(&block)
yield
expect(response).to have_http_status(429)
end
end end
# frozen_string_literal: true
module RackAttackSpecHelpers
def post_args_with_token_headers(url, token_headers)
[url, params: nil, headers: token_headers]
end
def api_get_args_with_token_headers(partial_url, token_headers)
["/api/#{API::API.version}#{partial_url}", params: nil, headers: token_headers]
end
def rss_url(user)
"/dashboard/projects.atom?feed_token=#{user.feed_token}"
end
def private_token_headers(user)
{ 'HTTP_PRIVATE_TOKEN' => user.private_token }
end
def personal_access_token_headers(personal_access_token)
{ 'HTTP_PRIVATE_TOKEN' => personal_access_token.token }
end
def oauth_token_headers(oauth_access_token)
{ 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" }
end
def expect_rejection(&block)
yield
expect(response).to have_http_status(429)
end
end
...@@ -36,6 +36,7 @@ module TestEnv ...@@ -36,6 +36,7 @@ module TestEnv
'expand-collapse-lines' => '238e82d', 'expand-collapse-lines' => '238e82d',
'pages-deploy' => '7897d5b', 'pages-deploy' => '7897d5b',
'pages-deploy-target' => '7975be0', 'pages-deploy-target' => '7975be0',
'audio' => 'c3c21fd',
'video' => '8879059', 'video' => '8879059',
'add-balsamiq-file' => 'b89b56d', 'add-balsamiq-file' => 'b89b56d',
'crlf-diff' => '5938907', 'crlf-diff' => '5938907',
......
...@@ -193,6 +193,17 @@ module MarkdownMatchers ...@@ -193,6 +193,17 @@ module MarkdownMatchers
end end
end end
# AudioLinkFilter
matcher :parse_audio_links do
set_default_markdown_messages
match do |actual|
audio = actual.at_css('audio')
expect(audio['src']).to end_with('/assets/audio/gitlab-demo.wav')
end
end
# ColorFilter # ColorFilter
matcher :parse_colors do matcher :parse_colors do
set_default_markdown_messages set_default_markdown_messages
......
# frozen_string_literal: true
shared_context 'rack attack cache store' do
around do |example|
# Instead of test environment's :null_store so the throttles can increment
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
# Make time-dependent tests deterministic
Timecop.freeze { example.run }
Rack::Attack.cache.store = Rails.cache
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