Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-10-09 12:06:13 +00:00
parent 3744bcc0d1
commit 0a850868df
96 changed files with 1314 additions and 260 deletions

View file

@ -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(),

View 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);
}
}

View file

@ -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');
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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"
/>

View file

@ -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

View file

@ -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>

View file

@ -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'));
},
},
};

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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') }}

View file

@ -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"
/>

View file

@ -7,7 +7,7 @@
*
* @example
* <clipboard-button
* title="Copy to clipboard"
* title="Copy"
* text="Content to be copied"
* css-class="btn-transparent"
* />

View file

@ -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 {

View file

@ -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

View file

@ -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]

View file

@ -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]}")

View file

@ -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]

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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')

View file

@ -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

View file

@ -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',

View file

@ -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')

View file

@ -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")

View file

@ -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'

View file

@ -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')

View file

@ -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)

View file

@ -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.")

View file

@ -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}"

View file

@ -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

View file

@ -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"))

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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')

View file

@ -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

View file

@ -0,0 +1,5 @@
---
title: Feature enabling embedded audio elements in markdown.
merge_request: 17860
author: Jesse Hall @jessehall3
type: added

View file

@ -0,0 +1,5 @@
---
title: Add property to enable metrics dashboards to be rearranged
merge_request: 16605
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: HTML-escape search term in empty message
merge_request: 18319
author:
type: security

View file

@ -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')

View file

@ -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: '%'

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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:

Binary file not shown.

View file

@ -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).

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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:

View 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

View file

@ -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

View file

@ -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')

View file

@ -26,6 +26,7 @@ module Banzai
Filter::ColorFilter,
Filter::MermaidFilter,
Filter::VideoLinkFilter,
Filter::AudioLinkFilter,
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
Filter::InlineMetricsFilter,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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"

View file

@ -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',

View file

@ -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

View file

@ -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

Binary file not shown.

View file

@ -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`

View file

@ -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>

View file

@ -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',
},
});
});

View file

@ -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')

View file

@ -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>&lt;h1&gt;foo&lt;/h1&gt;</code>")
expect(message).to be_html_safe
end
end

View file

@ -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();
});

View file

@ -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;

View file

@ -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',
});

View 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

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }
}
}

View file

@ -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

View file

@ -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

View 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

View file

@ -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',

View file

@ -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

View 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