Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3744bcc0d1
commit
0a850868df
96 changed files with 1314 additions and 260 deletions
|
@ -21,6 +21,7 @@ import Reference from './nodes/reference';
|
|||
|
||||
import TableOfContents from './nodes/table_of_contents';
|
||||
import Video from './nodes/video';
|
||||
import Audio from './nodes/audio';
|
||||
|
||||
import BulletList from './nodes/bullet_list';
|
||||
import OrderedList from './nodes/ordered_list';
|
||||
|
@ -78,6 +79,7 @@ export default [
|
|||
|
||||
new TableOfContents(),
|
||||
new Video(),
|
||||
new Audio(),
|
||||
|
||||
new BulletList(),
|
||||
new OrderedList(),
|
||||
|
|
53
app/assets/javascripts/behaviors/markdown/nodes/audio.js
Normal file
53
app/assets/javascripts/behaviors/markdown/nodes/audio.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
/* 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 {
|
|||
toggleCopyButtonState() {
|
||||
if (!this.copySourceBtn) return;
|
||||
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');
|
||||
} else if (this.activeViewer === this.simpleViewer) {
|
||||
this.copySourceBtn.setAttribute(
|
||||
'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');
|
||||
} else {
|
||||
this.copySourceBtn.setAttribute(
|
||||
'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');
|
||||
}
|
||||
|
|
|
@ -294,7 +294,7 @@ export default {
|
|||
<span class="input-group-append">
|
||||
<clipboard-button
|
||||
:text="ingressExternalEndpoint"
|
||||
:title="s__('ClusterIntegration|Copy Ingress Endpoint to clipboard')"
|
||||
:title="s__('ClusterIntegration|Copy Ingress Endpoint')"
|
||||
class="input-group-text js-clipboard-btn"
|
||||
/>
|
||||
</span>
|
||||
|
@ -472,7 +472,7 @@ export default {
|
|||
<span class="input-group-btn">
|
||||
<clipboard-button
|
||||
:text="jupyterHostname"
|
||||
:title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
|
||||
:title="s__('ClusterIntegration|Copy Jupyter Hostname')"
|
||||
class="js-clipboard-btn"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -103,7 +103,7 @@ export default {
|
|||
<span class="input-group-append">
|
||||
<clipboard-button
|
||||
: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"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -121,7 +121,7 @@ export default {
|
|||
<div class="label label-monospace monospace" v-text="commit.short_id"></div>
|
||||
<clipboard-button
|
||||
:text="commit.id"
|
||||
:title="__('Copy commit SHA to clipboard')"
|
||||
:title="__('Copy commit SHA')"
|
||||
class="btn btn-default"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -209,7 +209,7 @@ export default {
|
|||
</a>
|
||||
|
||||
<clipboard-button
|
||||
:title="__('Copy file path to clipboard')"
|
||||
:title="__('Copy file path')"
|
||||
:text="diffFile.file_path"
|
||||
:gfm="gfmCopyText"
|
||||
css-class="btn-default btn-transparent btn-clipboard"
|
||||
|
|
|
@ -41,7 +41,7 @@ export default {
|
|||
|
||||
<clipboard-button
|
||||
:text="commit.id"
|
||||
:title="__('Copy commit SHA to clipboard')"
|
||||
:title="__('Copy commit SHA')"
|
||||
css-class="btn btn-clipboard btn-transparent"
|
||||
/>
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import VueDraggable from 'vuedraggable';
|
||||
import {
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
|
@ -8,8 +11,6 @@ import {
|
|||
GlModalDirective,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import _ from 'underscore';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { __, s__ } from '~/locale';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
|
@ -26,6 +27,7 @@ let sidebarMutationObserver;
|
|||
|
||||
export default {
|
||||
components: {
|
||||
VueDraggable,
|
||||
MonitorTimeSeriesChart,
|
||||
MonitorSingleStatChart,
|
||||
PanelType,
|
||||
|
@ -151,6 +153,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
rearrangePanelsAvailable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -160,6 +167,7 @@ export default {
|
|||
selectedTimeWindowKey: '',
|
||||
formIsValid: null,
|
||||
timeWindows: {},
|
||||
isRearrangingPanels: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -183,6 +191,9 @@ export default {
|
|||
selectedDashboardText() {
|
||||
return this.currentDashboard || this.firstDashboard.display_name;
|
||||
},
|
||||
showRearrangePanelsBtn() {
|
||||
return !this.showEmptyState && this.rearrangePanelsAvailable;
|
||||
},
|
||||
addingMetricsAvailable() {
|
||||
return IS_EE && this.canAddMetrics && !this.showEmptyState;
|
||||
},
|
||||
|
@ -271,9 +282,14 @@ export default {
|
|||
return Object.values(this.getGraphAlerts(queries));
|
||||
},
|
||||
showToast() {
|
||||
this.$toast.show(__('Link copied to clipboard'));
|
||||
this.$toast.show(__('Link copied'));
|
||||
},
|
||||
// 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) {
|
||||
const dashboard = this.currentDashboard || this.firstDashboard.path;
|
||||
const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null);
|
||||
|
@ -287,6 +303,9 @@ export default {
|
|||
this.elWidth = this.$el.clientWidth;
|
||||
}, sidebarAnimationDuration);
|
||||
},
|
||||
toggleRearrangingPanels() {
|
||||
this.isRearrangingPanels = !this.isRearrangingPanels;
|
||||
},
|
||||
setFormValidity(isValid) {
|
||||
this.formIsValid = isValid;
|
||||
},
|
||||
|
@ -389,15 +408,27 @@ export default {
|
|||
</template>
|
||||
|
||||
<gl-form-group
|
||||
v-if="addingMetricsAvailable || externalDashboardUrl.length"
|
||||
v-if="addingMetricsAvailable || showRearrangePanelsBtn || externalDashboardUrl.length"
|
||||
label-for="prometheus-graphs-dropdown-buttons"
|
||||
class="dropdown-buttons col-lg d-lg-flex align-items-end"
|
||||
>
|
||||
<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
|
||||
v-if="addingMetricsAvailable"
|
||||
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 }}
|
||||
</gl-button>
|
||||
|
@ -451,17 +482,42 @@ export default {
|
|||
:collapse-group="groupHasData(groupData)"
|
||||
>
|
||||
<template v-if="additionalPanelTypesEnabled">
|
||||
<panel-type
|
||||
v-for="(graphData, graphIndex) in groupData.metrics"
|
||||
:key="`panel-type-${graphIndex}`"
|
||||
class="col-12 col-lg-6 pb-3"
|
||||
: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}`"
|
||||
/>
|
||||
<vue-draggable
|
||||
:list="groupData.metrics"
|
||||
group="metrics-dashboard"
|
||||
:component-data="{ attrs: { class: 'row mx-0 w-100' } }"
|
||||
:disabled="!isRearrangingPanels"
|
||||
>
|
||||
<div
|
||||
v-for="(graphData, graphIndex) in groupData.metrics"
|
||||
:key="`panel-type-${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 v-else>
|
||||
<monitor-time-series-chart
|
||||
|
|
|
@ -52,7 +52,7 @@ export default {
|
|||
<div
|
||||
v-if="collapseGroup"
|
||||
v-show="collapseGroup && showGroup"
|
||||
class="card-body prometheus-graph-group"
|
||||
class="card-body prometheus-graph-group p-0"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
|
|
@ -82,7 +82,7 @@ export default {
|
|||
return this.graphData.type && this.graphData.type === type;
|
||||
},
|
||||
showToast() {
|
||||
this.$toast.show(__('Link copied to clipboard'));
|
||||
this.$toast.show(__('Link copied'));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -143,7 +143,7 @@ export default {
|
|||
<span class="input-group-append">
|
||||
<clipboard-button
|
||||
:text="dockerBuildCommand"
|
||||
:title="s__('ContainerRegistry|Copy build command to clipboard')"
|
||||
:title="s__('ContainerRegistry|Copy build command')"
|
||||
class="input-group-text"
|
||||
/>
|
||||
</span>
|
||||
|
@ -154,7 +154,7 @@ export default {
|
|||
<span class="input-group-append">
|
||||
<clipboard-button
|
||||
:text="dockerPushCommand"
|
||||
:title="s__('ContainerRegistry|Copy push command to clipboard')"
|
||||
:title="s__('ContainerRegistry|Copy push command')"
|
||||
class="input-group-text"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
@ -144,7 +144,7 @@ export default {
|
|||
</div>
|
||||
<clipboard-button
|
||||
:text="commit.sha"
|
||||
:title="__('Copy commit SHA to clipboard')"
|
||||
:title="__('Copy commit SHA')"
|
||||
tooltip-placement="bottom"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -23,7 +23,7 @@ export default {
|
|||
<div class="url-text-field label label-monospace monospace">{{ uri }}</div>
|
||||
<clipboard-button
|
||||
:text="uri"
|
||||
:title="s__('ServerlessURL|Copy URL to clipboard')"
|
||||
:title="s__('ServerlessURL|Copy URL')"
|
||||
class="input-group-text js-clipboard-btn"
|
||||
/>
|
||||
<gl-button
|
||||
|
|
|
@ -90,7 +90,7 @@ export default {
|
|||
v-html="mr.sourceBranchLink"
|
||||
/><clipboard-button
|
||||
:text="branchNameClipboardData"
|
||||
:title="__('Copy branch name to clipboard')"
|
||||
:title="__('Copy branch name')"
|
||||
css-class="btn-default btn-transparent btn-clipboard"
|
||||
/>
|
||||
{{ s__('mrWidget|into') }}
|
||||
|
|
|
@ -170,7 +170,7 @@ export default {
|
|||
>
|
||||
</a>
|
||||
<clipboard-button
|
||||
:title="__('Copy commit SHA to clipboard')"
|
||||
:title="__('Copy commit SHA')"
|
||||
:text="mr.mergeCommitSha"
|
||||
css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha"
|
||||
/>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*
|
||||
* @example
|
||||
* <clipboard-button
|
||||
* title="Copy to clipboard"
|
||||
* title="Copy"
|
||||
* text="Content to be copied"
|
||||
* css-class="btn-transparent"
|
||||
* />
|
||||
|
|
|
@ -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 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
@ -22,11 +53,11 @@
|
|||
.prometheus-graph-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: $gl-padding / 2;
|
||||
margin-top: $gl-padding-8;
|
||||
}
|
||||
|
||||
.prometheus-graph {
|
||||
padding: $gl-padding / 2;
|
||||
padding: $gl-padding-8;
|
||||
}
|
||||
|
||||
.prometheus-graph-embed {
|
||||
|
|
|
@ -37,7 +37,7 @@ module UploadsActions
|
|||
expires_in 0.seconds, must_revalidate: true, private: true
|
||||
end
|
||||
|
||||
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
|
||||
disposition = uploader.embeddable? ? 'inline' : 'attachment'
|
||||
|
||||
uploaders = [uploader, *uploader.versions.values]
|
||||
uploader = uploaders.find { |version| version.filename == params[:filename] }
|
||||
|
@ -112,8 +112,8 @@ module UploadsActions
|
|||
uploader
|
||||
end
|
||||
|
||||
def image_or_video?
|
||||
uploader && uploader.exists? && uploader.image_or_video?
|
||||
def embeddable?
|
||||
uploader && uploader.exists? && uploader.embeddable?
|
||||
end
|
||||
|
||||
def find_model
|
||||
|
|
|
@ -4,7 +4,7 @@ class Groups::UploadsController < Groups::ApplicationController
|
|||
include UploadsActions
|
||||
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 :verify_workhorse_api!, only: [:authorize]
|
||||
|
|
|
@ -40,8 +40,8 @@ class HelpController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# Allow access to images in the doc folder
|
||||
format.any(:png, :gif, :jpeg, :mp4) do
|
||||
# Allow access to specific media files in the doc folder
|
||||
format.any(:png, :gif, :jpeg, :mp4, :mp3) do
|
||||
# Note: We are purposefully NOT using `Rails.root.join`
|
||||
path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}")
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ class Projects::UploadsController < Projects::ApplicationController
|
|||
|
||||
# These will kick you out if you don't have access.
|
||||
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 :verify_workhorse_api!, only: [:authorize]
|
||||
|
|
|
@ -197,13 +197,13 @@ module BlobHelper
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def copy_blob_source_button(blob)
|
||||
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
|
||||
|
||||
def open_raw_blob_button(blob)
|
||||
|
|
|
@ -21,7 +21,7 @@ module ButtonHelper
|
|||
# See http://clipboardjs.com/#usage
|
||||
def clipboard_button(data = {})
|
||||
css_class = data[:class] || 'btn-clipboard btn-transparent'
|
||||
title = data[:title] || _('Copy to clipboard')
|
||||
title = data[:title] || _('Copy')
|
||||
button_text = data[:button_text] || ''
|
||||
hide_tooltip = data[:hide_tooltip] || false
|
||||
hide_button_icon = data[:hide_button_icon] || false
|
||||
|
|
|
@ -79,7 +79,7 @@ module SearchHelper
|
|||
def search_entries_empty_message(scope, term)
|
||||
(s_("SearchResults|We couldn't find any %{scope} matching %{term}") % {
|
||||
scope: search_entries_scope_label(scope, 0),
|
||||
term: "<code>#{term}</code>"
|
||||
term: "<code>#{h(term)}</code>"
|
||||
}).html_safe
|
||||
end
|
||||
|
||||
|
|
|
@ -109,6 +109,9 @@ module ApplicationSettingImplementation
|
|||
throttle_protected_paths_in_seconds: 10,
|
||||
throttle_protected_paths_per_period: 60,
|
||||
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,
|
||||
two_factor_grace_period: 48,
|
||||
unique_ips_limit_enabled: false,
|
||||
|
|
|
@ -179,6 +179,10 @@ class Blob < SimpleDelegator
|
|||
UploaderHelper::SAFE_VIDEO_EXT.include?(extension)
|
||||
end
|
||||
|
||||
def audio?
|
||||
UploaderHelper::SAFE_AUDIO_EXT.include?(extension)
|
||||
end
|
||||
|
||||
def readable_text?
|
||||
text_in_repo? && !stored_externally? && !truncated?
|
||||
end
|
||||
|
|
|
@ -415,7 +415,7 @@ class Commit
|
|||
|
||||
if entry[:type] == :blob
|
||||
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
|
||||
entry[:type]
|
||||
end
|
||||
|
|
|
@ -45,3 +45,5 @@
|
|||
= _('Configure paths to be protected by Rack Attack. A web server restart is required after changing these settings.')
|
||||
.settings-content
|
||||
= render 'protected_paths'
|
||||
|
||||
= render_if_exists 'admin/application_settings/ee_network_settings'
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
.input-group
|
||||
%input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
|
||||
.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
|
||||
%td
|
||||
= _('Secret')
|
||||
|
@ -22,7 +22,7 @@
|
|||
.input-group
|
||||
%input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
|
||||
.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
|
||||
%td
|
||||
= _('Callback URL')
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
- if @new_impersonation_token
|
||||
= render "shared/personal_access_tokens_created_container", new_token_value: @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
|
||||
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
%li
|
||||
= _("Specify the following URL during the Runner setup:")
|
||||
%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
|
||||
= _("Use the following registration token during setup:")
|
||||
%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
|
||||
= button_to _("Reset runners registration token"), reset_token_url,
|
||||
method: :put, class: 'btn btn-default',
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
.input-group
|
||||
%input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
|
||||
.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
|
||||
%td
|
||||
= _('Secret')
|
||||
|
@ -25,7 +25,7 @@
|
|||
.input-group
|
||||
%input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
|
||||
.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
|
||||
%td
|
||||
= _('Callback URL')
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
%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") }
|
||||
%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
|
||||
= 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 @@
|
|||
.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' }
|
||||
.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'
|
||||
- if http_enabled?
|
||||
%li.pt-2
|
||||
|
@ -22,7 +22,7 @@
|
|||
.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' }
|
||||
.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/kerberos_clone_field'
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
%strong
|
||||
#{ s_('CommitBoxTitle|Commit') }
|
||||
%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')
|
||||
#{time_ago_with_tooltip(@commit.authored_date)}
|
||||
%span= s_('ByAuthor|by')
|
||||
|
|
|
@ -56,5 +56,5 @@
|
|||
.commit-sha-group.d-none.d-sm-flex
|
||||
.label.label-monospace.monospace
|
||||
= 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)
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
.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'
|
||||
.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.")
|
||||
|
||||
.form-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'
|
||||
.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.")
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
%p
|
||||
%strong Step 1.
|
||||
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
|
||||
- if @merge_request.for_fork?
|
||||
:preserve
|
||||
|
@ -27,7 +27,7 @@
|
|||
%p
|
||||
%strong Step 3.
|
||||
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
|
||||
- if @merge_request.for_fork?
|
||||
:preserve
|
||||
|
@ -42,7 +42,7 @@
|
|||
%p
|
||||
%strong Step 4.
|
||||
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
|
||||
:preserve
|
||||
git push origin "#{h @merge_request.target_branch}"
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
= custom_icon('ellipsis_v')
|
||||
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
|
||||
%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
|
||||
%li
|
||||
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
|
||||
|
|
|
@ -58,4 +58,4 @@
|
|||
= sprite_icon('ellipsis_h', size: 12)
|
||||
%span.js-details-content.hide
|
||||
= 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 @@
|
|||
%td
|
||||
- if trigger.has_token_exposed?
|
||||
%span= trigger.token
|
||||
= clipboard_button(text: trigger.token, title: "Copy trigger token to clipboard")
|
||||
= clipboard_button(text: trigger.token, title: _("Copy trigger token"))
|
||||
- else
|
||||
%span= trigger.short_token
|
||||
|
||||
|
|
|
@ -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' }
|
||||
.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'
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- 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
|
||||
%h5.prepend-top-0
|
||||
|
|
|
@ -148,13 +148,13 @@
|
|||
- project_ref = issuable_sidebar[:reference]
|
||||
.block.project-reference
|
||||
.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
|
||||
%span
|
||||
= _('Reference:')
|
||||
%cite{ title: 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)
|
||||
.block.js-sidebar-move-issue-block
|
||||
|
|
|
@ -142,10 +142,10 @@
|
|||
- if milestone_ref.present?
|
||||
.block.reference
|
||||
.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
|
||||
%span
|
||||
Reference:
|
||||
%cite{ title: 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 @@
|
|||
%strong.embed-toggle-list-item= _("Share")
|
||||
%input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
|
||||
.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
|
||||
|
|
5
changelogs/unreleased/16654-audio-in-markdown.yml
Normal file
5
changelogs/unreleased/16654-audio-in-markdown.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Feature enabling embedded audio elements in markdown.
|
||||
merge_request: 17860
|
||||
author: Jesse Hall @jessehall3
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add property to enable metrics dashboards to be rearranged
|
||||
merge_request: 16605
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/33668-fix-search-term-xss.yml
Normal file
5
changelogs/unreleased/33668-fix-search-term-xss.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: HTML-escape search term in empty message
|
||||
merge_request: 18319
|
||||
author:
|
||||
type: security
|
|
@ -125,4 +125,5 @@ class Rack::Attack
|
|||
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')
|
||||
|
|
|
@ -30,7 +30,7 @@ en:
|
|||
origin: Origin
|
||||
line: line
|
||||
line_capitalized: Line
|
||||
copy_to_clipboard: Copy to clipboard
|
||||
copy_to_clipboard: Copy
|
||||
query_plan: Query Plan
|
||||
events: Events
|
||||
percent: '%'
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# 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 @@
|
|||
#
|
||||
# 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
|
||||
enable_extension "pg_trgm"
|
||||
|
@ -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_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.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 ["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"
|
||||
|
|
|
@ -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
|
||||
|
||||
### 500 error after login
|
||||
|
|
|
@ -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
|
||||
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
|
||||
when they are fetched.
|
||||
|
||||
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
|
||||
|
||||
|
@ -52,7 +52,7 @@ To install a Camo server as an asset proxy:
|
|||
|
||||
## 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.
|
||||
|
||||
For example, the following is a link to an image in Markdown:
|
||||
|
|
BIN
doc/user/img/markdown_audio.mp3
Normal file
BIN
doc/user/img/markdown_audio.mp3
Normal file
Binary file not shown.
|
@ -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) |
|
||||
| [emphasis](#emphasis) | [multiple underscores in words](#multiple-underscores-in-words-and-mid-word-emphasis)
|
||||
| [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) |
|
||||
| [links](#links) | [automatically linking URLs](#url-auto-linking) |
|
||||
|
||||
|
@ -899,6 +899,23 @@ Here's a sample video:
|
|||
|
||||
![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
|
||||
|
||||
> 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:
|
|||
|
||||
![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.
|
||||
|
|
|
@ -141,9 +141,9 @@ for the issue. This will automatically enable if you participate in the issue in
|
|||
|
||||
#### 14. Reference
|
||||
|
||||
- A quick "copy to clipboard" button for that issue's reference, which looks like `foo/bar#xxx`,
|
||||
where `foo` is the `username` or `groupname`, `bar` is the `project-name`, and
|
||||
`xxx` is the issue number.
|
||||
- A quick "copy" button for that issue's reference, which looks like
|
||||
`foo/bar#xxx`, where `foo` is the `username` or `groupname`, `bar` is the
|
||||
`project-name`, and `xxx` is the issue number.
|
||||
|
||||
#### 15. Edit
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ Here's how the process would look like:
|
|||
|
||||
![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:
|
||||
|
||||
```sh
|
||||
|
|
|
@ -70,8 +70,8 @@ To embed a snippet, first make sure that:
|
|||
- In **Project > Settings > Permissions**, the snippets permissions are
|
||||
set to **Everyone with access**
|
||||
|
||||
Once the above conditions are met, the "Embed" section will appear in your snippet
|
||||
where you can simply click on the "Copy to clipboard" button. This copies a one-line
|
||||
Once the above conditions are met, the "Embed" section will appear in your
|
||||
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.
|
||||
|
||||
Here's how an example code looks like:
|
||||
|
|
66
lib/banzai/filter/audio_link_filter.rb
Normal file
66
lib/banzai/filter/audio_link_filter.rb
Normal file
|
@ -0,0 +1,66 @@
|
|||
# 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
|
|||
el.attribute('href')
|
||||
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')]
|
||||
end
|
||||
|
||||
|
@ -83,7 +83,7 @@ module Banzai
|
|||
get_blob_types(paths).each do |name, type|
|
||||
if type == :blob
|
||||
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
|
||||
uri_types[name] = type
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ module Banzai
|
|||
|
||||
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|
|
||||
attr = el.attribute('data-src') || el.attribute('src')
|
||||
|
|
|
@ -26,6 +26,7 @@ module Banzai
|
|||
Filter::ColorFilter,
|
||||
Filter::MermaidFilter,
|
||||
Filter::VideoLinkFilter,
|
||||
Filter::AudioLinkFilter,
|
||||
Filter::ImageLazyLoadFilter,
|
||||
Filter::ImageLinkFilter,
|
||||
Filter::InlineMetricsFilter,
|
||||
|
|
|
@ -1018,7 +1018,7 @@ into similar problems in the future (e.g. when new tables are created).
|
|||
end
|
||||
|
||||
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
|
||||
# the same time, which is not helpful in most cases where we wish to
|
||||
|
|
|
@ -10,14 +10,14 @@ module Gitlab
|
|||
return unless name = markdown_name
|
||||
|
||||
markdown = "[#{name.gsub(']', '\\]')}](#{secure_url})"
|
||||
markdown = "!#{markdown}" if image_or_video? || dangerous_image_or_video?
|
||||
markdown = "!#{markdown}" if embeddable? || dangerous_embeddable?
|
||||
markdown
|
||||
end
|
||||
|
||||
def markdown_name
|
||||
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
|
||||
|
|
|
@ -26,11 +26,13 @@ module Gitlab
|
|||
# on IE >= 9.
|
||||
# http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
|
||||
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
|
||||
# proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
|
||||
DANGEROUS_IMAGE_EXT = %w[svg].freeze
|
||||
DANGEROUS_VIDEO_EXT = [].freeze # None, yet
|
||||
DANGEROUS_AUDIO_EXT = [].freeze # None, yet
|
||||
|
||||
def image?
|
||||
extension_match?(SAFE_IMAGE_EXT)
|
||||
|
@ -40,8 +42,12 @@ module Gitlab
|
|||
extension_match?(SAFE_VIDEO_EXT)
|
||||
end
|
||||
|
||||
def image_or_video?
|
||||
image? || video?
|
||||
def audio?
|
||||
extension_match?(SAFE_AUDIO_EXT)
|
||||
end
|
||||
|
||||
def embeddable?
|
||||
image? || video? || audio?
|
||||
end
|
||||
|
||||
def dangerous_image?
|
||||
|
@ -52,8 +58,12 @@ module Gitlab
|
|||
extension_match?(DANGEROUS_VIDEO_EXT)
|
||||
end
|
||||
|
||||
def dangerous_image_or_video?
|
||||
dangerous_image? || dangerous_video?
|
||||
def dangerous_audio?
|
||||
extension_match?(DANGEROUS_AUDIO_EXT)
|
||||
end
|
||||
|
||||
def dangerous_embeddable?
|
||||
dangerous_image? || dangerous_video? || dangerous_audio?
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -37,8 +37,7 @@ module Gitlab
|
|||
# - post_data: a string of raw POST data to use. Changes the HTTP verb to
|
||||
# POST.
|
||||
#
|
||||
# - user: a user to authenticate as. Only works if the user has a valid
|
||||
# personal access token.
|
||||
# - user: a user to authenticate as.
|
||||
#
|
||||
# - private_token: instead of providing a user instance, the token can be
|
||||
# given as a string. Takes precedence over the user option.
|
||||
|
|
|
@ -1889,6 +1889,9 @@ msgstr ""
|
|||
msgid "Are you sure? This will invalidate your registered applications and U2F devices."
|
||||
msgstr ""
|
||||
|
||||
msgid "Arrange charts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Artifact ID"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3399,13 +3402,13 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Copy CA Certificate"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Copy Ingress Endpoint to clipboard"
|
||||
msgid "ClusterIntegration|Copy Ingress Endpoint"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
|
||||
msgid "ClusterIntegration|Copy Jupyter Hostname"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Copy Knative Endpoint to clipboard"
|
||||
msgid "ClusterIntegration|Copy Knative Endpoint"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Copy Kubernetes cluster name"
|
||||
|
@ -4127,6 +4130,9 @@ msgstr ""
|
|||
msgid "Configure limits for web and API requests."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -4196,10 +4202,10 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Container Registry"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Copy build command to clipboard"
|
||||
msgid "ContainerRegistry|Copy build command"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Copy push command to clipboard"
|
||||
msgid "ContainerRegistry|Copy push command"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Docker connection error"
|
||||
|
@ -4330,16 +4336,19 @@ msgstr ""
|
|||
msgid "Copied labels and milestone from %{source_issuable_reference}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy %{http_label} clone URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy %{protocol} clone URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy %{proxy_url} to clipboard"
|
||||
msgid "Copy %{proxy_url}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy ID to clipboard"
|
||||
msgid "Copy ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy KRB5 clone URL"
|
||||
|
@ -4351,19 +4360,28 @@ msgstr ""
|
|||
msgid "Copy SSH public key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy URL to clipboard"
|
||||
msgid "Copy URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy branch name to clipboard"
|
||||
msgid "Copy branch name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy command to clipboard"
|
||||
msgid "Copy command"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy commit SHA to clipboard"
|
||||
msgid "Copy commands"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy file path to clipboard"
|
||||
msgid "Copy commit SHA"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy file contents"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy file path"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy impersonation token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy labels and milestone from %{source_issuable_reference}."
|
||||
|
@ -4375,22 +4393,19 @@ msgstr ""
|
|||
msgid "Copy link"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy personal access token to clipboard"
|
||||
msgid "Copy personal access token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy reference to clipboard"
|
||||
msgid "Copy reference"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy secret to clipboard"
|
||||
msgid "Copy secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy source to clipboard"
|
||||
msgid "Copy token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy to clipboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy token to clipboard"
|
||||
msgid "Copy trigger token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Could not add admins as members"
|
||||
|
@ -5166,10 +5181,10 @@ msgstr ""
|
|||
msgid "DeployTokens|Allows read-only access to the repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployTokens|Copy deploy token to clipboard"
|
||||
msgid "DeployTokens|Copy deploy token"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployTokens|Copy username to clipboard"
|
||||
msgid "DeployTokens|Copy username"
|
||||
msgstr ""
|
||||
|
||||
msgid "DeployTokens|Create deploy token"
|
||||
|
@ -5736,6 +5751,9 @@ msgstr ""
|
|||
msgid "Enable HTML emails"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Incident Management inbound alert limit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Pseudonymizer data collection"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8240,6 +8258,9 @@ msgstr ""
|
|||
msgid "Helps prevent bots from creating accounts."
|
||||
msgstr ""
|
||||
|
||||
msgid "Helps reduce alert volume (e.g. if creating too many issues)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Helps reduce request volume for protected paths"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8584,6 +8605,9 @@ msgstr ""
|
|||
msgid "In the next step, you'll be able to select the projects you want to import."
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident Management Limits"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incidents"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9464,7 +9488,7 @@ msgid_plural "Limited to showing %d events at most"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Link copied to clipboard"
|
||||
msgid "Link copied"
|
||||
msgstr ""
|
||||
|
||||
msgid "Linked emails (%{email_count})"
|
||||
|
@ -14384,7 +14408,7 @@ msgstr ""
|
|||
msgid "ServerlessDetails|pods in use"
|
||||
msgstr ""
|
||||
|
||||
msgid "ServerlessURL|Copy URL to clipboard"
|
||||
msgid "ServerlessURL|Copy URL"
|
||||
msgstr ""
|
||||
|
||||
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 ""
|
|||
msgid "Switch to GitLab Next"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch to the source to copy it to the clipboard"
|
||||
msgid "Switch to the source to copy the file contents"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync information"
|
||||
|
@ -17953,10 +17977,10 @@ msgstr ""
|
|||
msgid "VisualReviewApp|%{stepStart}Step 5%{stepEnd}. Leave feedback in the Review App."
|
||||
msgstr ""
|
||||
|
||||
msgid "VisualReviewApp|Copy merge request ID to clipboard"
|
||||
msgid "VisualReviewApp|Copy merge request ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "VisualReviewApp|Copy script to clipboard"
|
||||
msgid "VisualReviewApp|Copy script"
|
||||
msgstr ""
|
||||
|
||||
msgid "VisualReviewApp|Enable Visual Reviews"
|
||||
|
@ -18022,7 +18046,7 @@ msgstr ""
|
|||
msgid "Vulnerability|Severity"
|
||||
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 ""
|
||||
|
||||
msgid "Waiting for performance data"
|
||||
|
|
|
@ -178,6 +178,12 @@ describe 'Copy as GFM', :js do
|
|||
'![Video](https://example.com/video.mp4)'
|
||||
)
|
||||
|
||||
verify(
|
||||
'AudioLinkFilter',
|
||||
|
||||
'![Audio](https://example.com/audio.wav)'
|
||||
)
|
||||
|
||||
verify(
|
||||
'MathFilter: math as converted from GFM to HTML',
|
||||
|
||||
|
|
|
@ -320,6 +320,10 @@ describe 'GitLab Markdown', :aggregate_failures do
|
|||
expect(doc).to parse_video_links
|
||||
end
|
||||
|
||||
aggregate_failures 'AudioLinkFilter' do
|
||||
expect(doc).to parse_audio_links
|
||||
end
|
||||
|
||||
aggregate_failures 'ColorFilter' do
|
||||
expect(doc).to parse_colors
|
||||
end
|
||||
|
|
|
@ -101,7 +101,7 @@ describe 'Branches' do
|
|||
visit project_branches_filtered_path(project, state: 'all')
|
||||
|
||||
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)
|
||||
end
|
||||
|
|
BIN
spec/fixtures/audio_sample.wav
vendored
Normal file
BIN
spec/fixtures/audio_sample.wav
vendored
Normal file
Binary file not shown.
4
spec/fixtures/markdown.md.erb
vendored
4
spec/fixtures/markdown.md.erb
vendored
|
@ -286,6 +286,10 @@ However the wrapping tags cannot be mixed as such:
|
|||
|
||||
![My Video](/assets/videos/gitlab-demo.mp4)
|
||||
|
||||
### Audio
|
||||
|
||||
![My Audio Clip](/assets/audio/gitlab-demo.wav)
|
||||
|
||||
### Colors
|
||||
|
||||
`#F00`
|
||||
|
|
|
@ -90,7 +90,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
|
|||
<clipboardbutton-stub
|
||||
cssclass="btn-default"
|
||||
text="123456789"
|
||||
title="Copy commit SHA to clipboard"
|
||||
title="Copy commit SHA"
|
||||
tooltipplacement="bottom"
|
||||
/>
|
||||
</div>
|
||||
|
@ -193,7 +193,7 @@ exports[`Repository last commit component renders the signature HTML as returned
|
|||
<clipboardbutton-stub
|
||||
cssclass="btn-default"
|
||||
text="123456789"
|
||||
title="Copy commit SHA to clipboard"
|
||||
title="Copy commit SHA"
|
||||
tooltipplacement="bottom"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ describe('modal copy button', () => {
|
|||
wrapper = shallowMount(Component, {
|
||||
propsData: {
|
||||
text: 'copy me',
|
||||
title: 'Copy this value into Clipboard!',
|
||||
title: 'Copy this value',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -166,7 +166,7 @@ describe ButtonHelper 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('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-placement')).to eq('bottom')
|
||||
expect(element.attr('data-container')).to eq('body')
|
||||
|
|
|
@ -142,9 +142,9 @@ describe SearchHelper do
|
|||
|
||||
describe 'search_entries_empty_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><h1>foo</h1></code>")
|
||||
expect(message).to be_html_safe
|
||||
end
|
||||
end
|
||||
|
|
|
@ -101,7 +101,7 @@ describe('Blob viewer', () => {
|
|||
|
||||
it('has tooltip when disabled', () => {
|
||||
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', () => {
|
|||
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(copyButton.getAttribute('data-original-title')).toBe('Copy source to clipboard');
|
||||
expect(copyButton.getAttribute('data-original-title')).toBe('Copy file contents');
|
||||
|
||||
done();
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { GlToast } from '@gitlab/ui';
|
||||
import VueDraggable from 'vuedraggable';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Dashboard from '~/monitoring/components/dashboard.vue';
|
||||
import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
|
||||
|
@ -51,11 +52,6 @@ describe('Dashboard', () => {
|
|||
<div class="layout-page"></div>
|
||||
`);
|
||||
|
||||
window.gon = {
|
||||
...window.gon,
|
||||
ee: false,
|
||||
};
|
||||
|
||||
store = createStore();
|
||||
mock = new MockAdapter(axios);
|
||||
DashboardComponent = Vue.extend(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
|
||||
xdescribe('link to chart', () => {
|
||||
let wrapper;
|
||||
|
|
|
@ -14,7 +14,7 @@ describe('clipboard button', () => {
|
|||
beforeEach(() => {
|
||||
vm = mountComponent(Component, {
|
||||
text: 'copy me',
|
||||
title: 'Copy this value into Clipboard!',
|
||||
title: 'Copy this value',
|
||||
cssClass: 'btn-danger',
|
||||
});
|
||||
});
|
||||
|
@ -26,7 +26,7 @@ describe('clipboard button', () => {
|
|||
});
|
||||
|
||||
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', () => {
|
||||
|
@ -39,7 +39,7 @@ describe('clipboard button', () => {
|
|||
vm = mountComponent(Component, {
|
||||
text: 'copy me',
|
||||
gfm: '`path/to/file`',
|
||||
title: 'Copy this value into Clipboard!',
|
||||
title: 'Copy this value',
|
||||
cssClass: 'btn-danger',
|
||||
});
|
||||
|
||||
|
|
120
spec/lib/banzai/filter/audio_link_filter_spec.rb
Normal file
120
spec/lib/banzai/filter/audio_link_filter_spec.rb
Normal file
|
@ -0,0 +1,120 @@
|
|||
# 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
|
|||
%(<video src="#{path}"></video>)
|
||||
end
|
||||
|
||||
def audio(path)
|
||||
%(<audio src="#{path}"></audio>)
|
||||
end
|
||||
|
||||
def link(path)
|
||||
%(<a href="#{path}">#{path}</a>)
|
||||
end
|
||||
|
@ -82,6 +86,12 @@ describe Banzai::Filter::RelativeLinkFilter do
|
|||
|
||||
expect(doc.at_css('video')['src']).to eq 'files/videos/intro.mp4'
|
||||
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
|
||||
|
||||
context 'with a project_wiki' do
|
||||
|
@ -218,6 +228,13 @@ describe Banzai::Filter::RelativeLinkFilter do
|
|||
.to eq "/#{project_path}/raw/video/files/videos/intro.mp4"
|
||||
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
|
||||
doc = filter(link('#section-1'))
|
||||
expect(doc.at_css('a')['href']).to eq '#section-1'
|
||||
|
|
|
@ -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")
|
||||
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
|
||||
|
||||
describe "invalid links" do
|
||||
|
|
|
@ -260,11 +260,11 @@ describe Banzai::Pipeline::WikiPipeline do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'videos' do
|
||||
let(:namespace) { create(:namespace, name: "wiki_link_ns") }
|
||||
let(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) }
|
||||
let(:project_wiki) { ProjectWiki.new(project, double(:user)) }
|
||||
let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
|
||||
describe 'videos and audio' do
|
||||
let_it_be(:namespace) { create(:namespace, name: "wiki_link_ns") }
|
||||
let_it_be(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) }
|
||||
let_it_be(:project_wiki) { ProjectWiki.new(project, double(:user)) }
|
||||
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
|
||||
markdown = "![video_file](video_file_name.mp4)"
|
||||
|
@ -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"')
|
||||
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
|
||||
|
|
|
@ -27,19 +27,35 @@ describe Gitlab::FileMarkdownLinkBuilder do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when file is an image or video' do
|
||||
let(:filename) { 'dk.png' }
|
||||
context 'when file is an image' do
|
||||
let(:filename) { 'my_image.png' }
|
||||
|
||||
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
|
||||
|
||||
context 'when file is not an image or video' do
|
||||
let(:filename) { 'dk.zip' }
|
||||
context 'when file is video' do
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -53,19 +69,35 @@ describe Gitlab::FileMarkdownLinkBuilder do
|
|||
end
|
||||
|
||||
describe 'mardown_name' do
|
||||
context 'when file is an image or video' do
|
||||
let(:filename) { 'dk.png' }
|
||||
context 'when file is an image' do
|
||||
let(:filename) { 'my_image.png' }
|
||||
|
||||
it 'retrieves the name without the extension' do
|
||||
expect(custom_class.markdown_name).to eq 'dk'
|
||||
expect(custom_class.markdown_name).to eq 'my_image'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file is not an image or video' do
|
||||
let(:filename) { 'dk.zip' }
|
||||
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
|
||||
expect(custom_class.markdown_name).to eq 'my_audio'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file is not embeddable' do
|
||||
let(:filename) { 'my_zip.zip' }
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -3,7 +3,21 @@ require 'spec_helper'
|
|||
|
||||
describe Gitlab::FileTypeDetection do
|
||||
context 'when class is an uploader' do
|
||||
shared_examples '#image? for an uploader' do
|
||||
let(:uploader) do
|
||||
example_uploader = Class.new(CarrierWave::Uploader::Base) do
|
||||
include Gitlab::FileTypeDetection
|
||||
|
||||
storage :file
|
||||
end
|
||||
|
||||
example_uploader.new
|
||||
end
|
||||
|
||||
def upload_fixture(filename)
|
||||
fixture_file_upload(File.join('spec', 'fixtures', filename))
|
||||
end
|
||||
|
||||
describe '#image?' do
|
||||
it 'returns true for an image file' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
|
||||
|
@ -23,6 +37,12 @@ describe Gitlab::FileTypeDetection do
|
|||
expect(uploader).not_to be_image
|
||||
end
|
||||
|
||||
it 'returns false for an audio file' do
|
||||
uploader.store!(upload_fixture('audio_sample.wav'))
|
||||
|
||||
expect(uploader).not_to be_image
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
|
||||
|
@ -32,7 +52,7 @@ describe Gitlab::FileTypeDetection do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples '#video? for an uploader' do
|
||||
describe '#video?' do
|
||||
it 'returns true for a video file' do
|
||||
uploader.store!(upload_fixture('video_sample.mp4'))
|
||||
|
||||
|
@ -45,8 +65,21 @@ describe Gitlab::FileTypeDetection do
|
|||
expect(uploader).not_to be_video
|
||||
end
|
||||
|
||||
it 'returns false for an audio file' do
|
||||
uploader.store!(upload_fixture('audio_sample.wav'))
|
||||
|
||||
expect(uploader).not_to be_video
|
||||
end
|
||||
|
||||
it 'returns false if file has a dangerous image extension' do
|
||||
uploader.store!(upload_fixture('unsanitized.svg'))
|
||||
|
||||
expect(uploader).to be_dangerous_image
|
||||
expect(uploader).not_to be_video
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
uploader.store!(upload_fixture('video_sample.mp4'))
|
||||
|
||||
allow(uploader).to receive(:filename).and_return(nil)
|
||||
|
||||
|
@ -54,7 +87,83 @@ describe Gitlab::FileTypeDetection do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples '#dangerous_image? for an uploader' do
|
||||
describe '#audio?' do
|
||||
it 'returns true for an audio file' do
|
||||
uploader.store!(upload_fixture('audio_sample.wav'))
|
||||
|
||||
expect(uploader).to be_audio
|
||||
end
|
||||
|
||||
it 'returns false for an image file' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
|
||||
expect(uploader).not_to be_audio
|
||||
end
|
||||
|
||||
it 'returns false for a video file' do
|
||||
uploader.store!(upload_fixture('video_sample.mp4'))
|
||||
|
||||
expect(uploader).not_to be_audio
|
||||
end
|
||||
|
||||
it 'returns false if file has a dangerous image extension' do
|
||||
uploader.store!(upload_fixture('unsanitized.svg'))
|
||||
|
||||
expect(uploader).to be_dangerous_image
|
||||
expect(uploader).not_to be_audio
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
uploader.store!(upload_fixture('audio_sample.wav'))
|
||||
|
||||
allow(uploader).to receive(:filename).and_return(nil)
|
||||
|
||||
expect(uploader).not_to be_audio
|
||||
end
|
||||
end
|
||||
|
||||
describe '#embeddable?' do
|
||||
it 'returns true for an image file' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
|
||||
expect(uploader).to be_embeddable
|
||||
end
|
||||
|
||||
it 'returns true for a video file' do
|
||||
uploader.store!(upload_fixture('video_sample.mp4'))
|
||||
|
||||
expect(uploader).to be_embeddable
|
||||
end
|
||||
|
||||
it 'returns true for an audio file' do
|
||||
uploader.store!(upload_fixture('audio_sample.wav'))
|
||||
|
||||
expect(uploader).to be_embeddable
|
||||
end
|
||||
|
||||
it 'returns false if not an embeddable file' do
|
||||
uploader.store!(upload_fixture('doc_sample.txt'))
|
||||
|
||||
expect(uploader).not_to be_embeddable
|
||||
end
|
||||
|
||||
it 'returns false if filename has a dangerous image extension' do
|
||||
uploader.store!(upload_fixture('unsanitized.svg'))
|
||||
|
||||
expect(uploader).to be_dangerous_image
|
||||
expect(uploader).not_to be_embeddable
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
|
||||
allow(uploader).to receive(:filename).and_return(nil)
|
||||
|
||||
expect(uploader).not_to be_embeddable
|
||||
end
|
||||
end
|
||||
|
||||
describe '#dangerous_image?' do
|
||||
it 'returns true if filename has a dangerous extension' do
|
||||
uploader.store!(upload_fixture('unsanitized.svg'))
|
||||
|
||||
|
@ -73,6 +182,12 @@ describe Gitlab::FileTypeDetection do
|
|||
expect(uploader).not_to be_dangerous_image
|
||||
end
|
||||
|
||||
it 'returns false for an audio file' do
|
||||
uploader.store!(upload_fixture('audio_sample.wav'))
|
||||
|
||||
expect(uploader).not_to be_dangerous_image
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
|
||||
|
@ -82,7 +197,7 @@ describe Gitlab::FileTypeDetection do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples '#dangerous_video? for an uploader' do
|
||||
describe '#dangerous_video?' do
|
||||
it 'returns false for a safe video file' do
|
||||
uploader.store!(upload_fixture('video_sample.mp4'))
|
||||
|
||||
|
@ -101,6 +216,12 @@ describe Gitlab::FileTypeDetection do
|
|||
expect(uploader).not_to be_dangerous_video
|
||||
end
|
||||
|
||||
it 'returns false for an audio file' do
|
||||
uploader.store!(upload_fixture('audio_sample.wav'))
|
||||
|
||||
expect(uploader).not_to be_dangerous_video
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
|
||||
|
@ -110,49 +231,91 @@ describe Gitlab::FileTypeDetection do
|
|||
end
|
||||
end
|
||||
|
||||
let(:uploader) do
|
||||
example_uploader = Class.new(CarrierWave::Uploader::Base) do
|
||||
include Gitlab::FileTypeDetection
|
||||
describe '#dangerous_audio?' do
|
||||
it 'returns false for a safe audio file' do
|
||||
uploader.store!(upload_fixture('audio_sample.wav'))
|
||||
|
||||
storage :file
|
||||
expect(uploader).not_to be_dangerous_audio
|
||||
end
|
||||
|
||||
example_uploader.new
|
||||
it 'returns false if filename is a dangerous image extension' do
|
||||
uploader.store!(upload_fixture('unsanitized.svg'))
|
||||
|
||||
expect(uploader).not_to be_dangerous_audio
|
||||
end
|
||||
|
||||
it 'returns false for an image file' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
|
||||
expect(uploader).not_to be_dangerous_audio
|
||||
end
|
||||
|
||||
it 'returns false for an video file' do
|
||||
uploader.store!(upload_fixture('video_sample.mp4'))
|
||||
|
||||
expect(uploader).not_to be_dangerous_audio
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
|
||||
allow(uploader).to receive(:filename).and_return(nil)
|
||||
|
||||
expect(uploader).not_to be_dangerous_audio
|
||||
end
|
||||
end
|
||||
|
||||
def upload_fixture(filename)
|
||||
fixture_file_upload(File.join('spec', 'fixtures', filename))
|
||||
end
|
||||
describe '#dangerous_embeddable?' do
|
||||
it 'returns true if filename has a dangerous image extension' do
|
||||
uploader.store!(upload_fixture('unsanitized.svg'))
|
||||
|
||||
describe '#image?' do
|
||||
include_examples '#image? for an uploader'
|
||||
end
|
||||
expect(uploader).to be_dangerous_embeddable
|
||||
end
|
||||
|
||||
describe '#video?' do
|
||||
include_examples '#video? for an uploader'
|
||||
end
|
||||
it 'returns false for an image file' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
|
||||
describe '#image_or_video?' do
|
||||
include_examples '#image? for an uploader'
|
||||
include_examples '#video? for an uploader'
|
||||
end
|
||||
expect(uploader).not_to be_dangerous_embeddable
|
||||
end
|
||||
|
||||
describe '#dangerous_image?' do
|
||||
include_examples '#dangerous_image? for an uploader'
|
||||
end
|
||||
it 'returns false for a video file' do
|
||||
uploader.store!(upload_fixture('video_sample.mp4'))
|
||||
|
||||
describe '#dangerous_video?' do
|
||||
include_examples '#dangerous_video? for an uploader'
|
||||
end
|
||||
expect(uploader).not_to be_dangerous_embeddable
|
||||
end
|
||||
|
||||
describe '#dangerous_image_or_video?' do
|
||||
include_examples '#dangerous_image? for an uploader'
|
||||
include_examples '#dangerous_video? for an uploader'
|
||||
it 'returns false for an audio file' do
|
||||
uploader.store!(upload_fixture('audio_sample.wav'))
|
||||
|
||||
expect(uploader).not_to be_dangerous_embeddable
|
||||
end
|
||||
|
||||
it 'returns false for a non-embeddable file' do
|
||||
uploader.store!(upload_fixture('doc_sample.txt'))
|
||||
|
||||
expect(uploader).not_to be_dangerous_embeddable
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
uploader.store!(upload_fixture('dk.png'))
|
||||
|
||||
allow(uploader).to receive(:filename).and_return(nil)
|
||||
|
||||
expect(uploader).not_to be_dangerous_embeddable
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when class is a regular class' do
|
||||
shared_examples '#image? for a regular class' do
|
||||
let(:custom_class) do
|
||||
custom_class = Class.new do
|
||||
include Gitlab::FileTypeDetection
|
||||
end
|
||||
|
||||
custom_class.new
|
||||
end
|
||||
|
||||
describe '#image?' do
|
||||
it 'returns true for an image file' do
|
||||
allow(custom_class).to receive(:filename).and_return('dk.png')
|
||||
|
||||
|
@ -166,12 +329,18 @@ describe Gitlab::FileTypeDetection do
|
|||
expect(custom_class).not_to be_image
|
||||
end
|
||||
|
||||
it 'returns false for any non image file' do
|
||||
it 'returns false for a video file' do
|
||||
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
|
||||
|
||||
expect(custom_class).not_to be_image
|
||||
end
|
||||
|
||||
it 'returns false for an audio file' do
|
||||
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
|
||||
|
||||
expect(custom_class).not_to be_image
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
allow(custom_class).to receive(:filename).and_return(nil)
|
||||
|
||||
|
@ -179,19 +348,25 @@ describe Gitlab::FileTypeDetection do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples '#video? for a regular class' do
|
||||
describe '#video?' do
|
||||
it 'returns true for a video file' do
|
||||
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
|
||||
|
||||
expect(custom_class).to be_video
|
||||
end
|
||||
|
||||
it 'returns false for any non-video file' do
|
||||
it 'returns false for an image file' do
|
||||
allow(custom_class).to receive(:filename).and_return('dk.png')
|
||||
|
||||
expect(custom_class).not_to be_video
|
||||
end
|
||||
|
||||
it 'returns false for an audio file' do
|
||||
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
|
||||
|
||||
expect(custom_class).not_to be_video
|
||||
end
|
||||
|
||||
it 'returns false if file has a dangerous image extension' do
|
||||
allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
|
||||
|
||||
|
@ -206,7 +381,79 @@ describe Gitlab::FileTypeDetection do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples '#dangerous_image? for a regular class' do
|
||||
describe '#audio?' do
|
||||
it 'returns true for an audio file' do
|
||||
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
|
||||
|
||||
expect(custom_class).to be_audio
|
||||
end
|
||||
|
||||
it 'returns false for an image file' do
|
||||
allow(custom_class).to receive(:filename).and_return('dk.png')
|
||||
|
||||
expect(custom_class).not_to be_audio
|
||||
end
|
||||
|
||||
it 'returns false for a video file' do
|
||||
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
|
||||
|
||||
expect(custom_class).not_to be_audio
|
||||
end
|
||||
|
||||
it 'returns false if file has a dangerous image extension' do
|
||||
allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
|
||||
|
||||
expect(custom_class).to be_dangerous_image
|
||||
expect(custom_class).not_to be_audio
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
allow(custom_class).to receive(:filename).and_return(nil)
|
||||
|
||||
expect(custom_class).not_to be_audio
|
||||
end
|
||||
end
|
||||
|
||||
describe '#embeddable?' do
|
||||
it 'returns true for an image file' do
|
||||
allow(custom_class).to receive(:filename).and_return('dk.png')
|
||||
|
||||
expect(custom_class).to be_embeddable
|
||||
end
|
||||
|
||||
it 'returns true for a video file' do
|
||||
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
|
||||
|
||||
expect(custom_class).to be_embeddable
|
||||
end
|
||||
|
||||
it 'returns true for an audio file' do
|
||||
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
|
||||
|
||||
expect(custom_class).to be_embeddable
|
||||
end
|
||||
|
||||
it 'returns false if not an embeddable file' do
|
||||
allow(custom_class).to receive(:filename).and_return('doc_sample.txt')
|
||||
|
||||
expect(custom_class).not_to be_embeddable
|
||||
end
|
||||
|
||||
it 'returns false if filename has a dangerous image extension' do
|
||||
allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
|
||||
|
||||
expect(custom_class).to be_dangerous_image
|
||||
expect(custom_class).not_to be_embeddable
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
allow(custom_class).to receive(:filename).and_return(nil)
|
||||
|
||||
expect(custom_class).not_to be_embeddable
|
||||
end
|
||||
end
|
||||
|
||||
describe '#dangerous_image?' do
|
||||
it 'returns true if file has a dangerous image extension' do
|
||||
allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
|
||||
|
||||
|
@ -219,12 +466,18 @@ describe Gitlab::FileTypeDetection do
|
|||
expect(custom_class).not_to be_dangerous_image
|
||||
end
|
||||
|
||||
it 'returns false for any non image file' do
|
||||
it 'returns false for a video file' do
|
||||
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
|
||||
|
||||
expect(custom_class).not_to be_dangerous_image
|
||||
end
|
||||
|
||||
it 'returns false for an audio file' do
|
||||
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
|
||||
|
||||
expect(custom_class).not_to be_dangerous_image
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
allow(custom_class).to receive(:filename).and_return(nil)
|
||||
|
||||
|
@ -232,7 +485,7 @@ describe Gitlab::FileTypeDetection do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples '#dangerous_video? for a regular class' do
|
||||
describe '#dangerous_video?' do
|
||||
it 'returns false for a safe video file' do
|
||||
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
|
||||
|
||||
|
@ -245,6 +498,12 @@ describe Gitlab::FileTypeDetection do
|
|||
expect(custom_class).not_to be_dangerous_video
|
||||
end
|
||||
|
||||
it 'returns false for an audio file' do
|
||||
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
|
||||
|
||||
expect(custom_class).not_to be_dangerous_video
|
||||
end
|
||||
|
||||
it 'returns false if file has a dangerous image extension' do
|
||||
allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
|
||||
|
||||
|
@ -258,38 +517,74 @@ describe Gitlab::FileTypeDetection do
|
|||
end
|
||||
end
|
||||
|
||||
let(:custom_class) do
|
||||
custom_class = Class.new do
|
||||
include Gitlab::FileTypeDetection
|
||||
describe '#dangerous_audio?' do
|
||||
it 'returns false for a safe audio file' do
|
||||
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
|
||||
|
||||
expect(custom_class).not_to be_dangerous_audio
|
||||
end
|
||||
|
||||
custom_class.new
|
||||
it 'returns false for an image file' do
|
||||
allow(custom_class).to receive(:filename).and_return('dk.png')
|
||||
|
||||
expect(custom_class).not_to be_dangerous_audio
|
||||
end
|
||||
|
||||
it 'returns false for a video file' do
|
||||
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
|
||||
|
||||
expect(custom_class).not_to be_dangerous_audio
|
||||
end
|
||||
|
||||
it 'returns false if file has a dangerous image extension' do
|
||||
allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
|
||||
|
||||
expect(custom_class).not_to be_dangerous_audio
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
allow(custom_class).to receive(:filename).and_return(nil)
|
||||
|
||||
expect(custom_class).not_to be_dangerous_audio
|
||||
end
|
||||
end
|
||||
|
||||
describe '#image?' do
|
||||
include_examples '#image? for a regular class'
|
||||
end
|
||||
describe '#dangerous_embeddable?' do
|
||||
it 'returns true if file has a dangerous image extension' do
|
||||
allow(custom_class).to receive(:filename).and_return('unsanitized.svg')
|
||||
|
||||
describe '#video?' do
|
||||
include_examples '#video? for a regular class'
|
||||
end
|
||||
expect(custom_class).to be_dangerous_embeddable
|
||||
end
|
||||
|
||||
describe '#image_or_video?' do
|
||||
include_examples '#image? for a regular class'
|
||||
include_examples '#video? for a regular class'
|
||||
end
|
||||
it 'returns false for an image file' do
|
||||
allow(custom_class).to receive(:filename).and_return('dk.png')
|
||||
|
||||
describe '#dangerous_image?' do
|
||||
include_examples '#dangerous_image? for a regular class'
|
||||
end
|
||||
expect(custom_class).not_to be_dangerous_embeddable
|
||||
end
|
||||
|
||||
describe '#dangerous_video?' do
|
||||
include_examples '#dangerous_video? for a regular class'
|
||||
end
|
||||
it 'returns false for a video file' do
|
||||
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
|
||||
|
||||
describe '#dangerous_image_or_video?' do
|
||||
include_examples '#dangerous_image? for a regular class'
|
||||
include_examples '#dangerous_video? for a regular class'
|
||||
expect(custom_class).not_to be_dangerous_embeddable
|
||||
end
|
||||
|
||||
it 'returns false for an audio file' do
|
||||
allow(custom_class).to receive(:filename).and_return('audio_sample.wav')
|
||||
|
||||
expect(custom_class).not_to be_dangerous_embeddable
|
||||
end
|
||||
|
||||
it 'returns false for a non-embeddable file' do
|
||||
allow(custom_class).to receive(:filename).and_return('doc_sample.txt')
|
||||
|
||||
expect(custom_class).not_to be_dangerous_embeddable
|
||||
end
|
||||
|
||||
it 'returns false if filename is blank' do
|
||||
allow(custom_class).to receive(:filename).and_return(nil)
|
||||
|
||||
expect(custom_class).not_to be_dangerous_embeddable
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,6 +43,11 @@ describe Gitlab::Utils::SanitizeNodeLink do
|
|||
doc: HTML::Pipeline.parse("<video><source src='#{scheme}alert(1);'></video>"),
|
||||
attr: "src",
|
||||
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
|
|||
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/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(commit.uri_type('files/js/application.js')).to be(:blob)
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'Rack Attack global throttles' do
|
||||
include RackAttackSpecHelpers
|
||||
|
||||
let(:settings) { Gitlab::CurrentSettings.current_application_settings }
|
||||
|
||||
# Start with really high limits and override them with low limits to ensure
|
||||
|
@ -22,15 +24,7 @@ describe 'Rack Attack global throttles' do
|
|||
let(:period_in_seconds) { 10000 }
|
||||
let(:period) { period_in_seconds.seconds }
|
||||
|
||||
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
|
||||
include_context 'rack attack cache store'
|
||||
|
||||
describe 'unauthenticated requests' do
|
||||
let(:url_that_does_not_require_authentication) { '/users/sign_in' }
|
||||
|
@ -361,30 +355,4 @@ describe 'Rack Attack global throttles' do
|
|||
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
|
||||
|
|
33
spec/support/helpers/rack_attack_spec_helpers.rb
Normal file
33
spec/support/helpers/rack_attack_spec_helpers.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
# 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
|
|||
'expand-collapse-lines' => '238e82d',
|
||||
'pages-deploy' => '7897d5b',
|
||||
'pages-deploy-target' => '7975be0',
|
||||
'audio' => 'c3c21fd',
|
||||
'video' => '8879059',
|
||||
'add-balsamiq-file' => 'b89b56d',
|
||||
'crlf-diff' => '5938907',
|
||||
|
|
|
@ -193,6 +193,17 @@ module MarkdownMatchers
|
|||
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
|
||||
matcher :parse_colors do
|
||||
set_default_markdown_messages
|
||||
|
|
13
spec/support/shared_contexts/rack_attack_shared_context.rb
Normal file
13
spec/support/shared_contexts/rack_attack_shared_context.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# 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
|
Loading…
Reference in a new issue