diff options
96 files changed, 1314 insertions, 260 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/editor_extensions.js b/app/assets/javascripts/behaviors/markdown/editor_extensions.js index 47e5fc65c48..8bd2145db1c 100644 --- a/app/assets/javascripts/behaviors/markdown/editor_extensions.js +++ b/app/assets/javascripts/behaviors/markdown/editor_extensions.js @@ -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(), diff --git a/app/assets/javascripts/behaviors/markdown/nodes/audio.js b/app/assets/javascripts/behaviors/markdown/nodes/audio.js new file mode 100644 index 00000000000..48ac408cf24 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/audio.js @@ -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); + } +} diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 9ea455069f3..07e4dde41d9 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -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'); } diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 4d3e759d8d4..b95f97077f6 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -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> diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue index e26ef135bc5..25347b11b6c 100644 --- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue +++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue @@ -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> diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index 761fd1583ed..43a7703f611 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -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> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index bfcc726a030..665328eb234 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -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" diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue index 9fac880c5f8..8156f26ffb1 100644 --- a/app/assets/javascripts/jobs/components/commit_block.vue +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -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" /> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 084f736911f..9ecb9324f8c 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -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 diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index 72ddd8d4fcf..ee3a2bae79b 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -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> diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index 73ff651d510..af0d8335c43 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -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')); }, }, }; diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 60aaef656a0..7ae06af02cf 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -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> diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue index 1b023f13862..19a2db2db25 100644 --- a/app/assets/javascripts/repository/components/last_commit.vue +++ b/app/assets/javascripts/repository/components/last_commit.vue @@ -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> diff --git a/app/assets/javascripts/serverless/components/url.vue b/app/assets/javascripts/serverless/components/url.vue index e47a03f1939..5e30c8d614e 100644 --- a/app/assets/javascripts/serverless/components/url.vue +++ b/app/assets/javascripts/serverless/components/url.vue @@ -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 diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index fb826be19f5..2aaba6e1c8a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -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') }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index fb07c03e34d..a2b5a79af36 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -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" /> diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index a620f560b52..9f498037185 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -7,7 +7,7 @@ * * @example * <clipboard-button - * title="Copy to clipboard" + * title="Copy" * text="Content to be copied" * css-class="btn-transparent" * /> diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss index 245efd91ec8..ceafff94719 100644 --- a/app/assets/stylesheets/pages/prometheus.scss +++ b/app/assets/stylesheets/pages/prometheus.scss @@ -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 { diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 79bee6f89d8..77afffc4b27 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -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 diff --git a/app/controllers/groups/uploads_controller.rb b/app/controllers/groups/uploads_controller.rb index 7e5cdae0ce3..3ae7e36c740 100644 --- a/app/controllers/groups/uploads_controller.rb +++ b/app/controllers/groups/uploads_controller.rb @@ -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] diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 837c26c630a..a58235790ad 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -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]}") diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 4ffcc2ac805..3e5a1cfc74d 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -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] diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 4b0713001a1..6d1ec16b0c2 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -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) diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 12cd5403f71..610d823dd3c 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -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 diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index c65611b7efc..9a19758b4e8 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -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 diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index d486347bb1d..e2579316fdd 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -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, diff --git a/app/models/blob.rb b/app/models/blob.rb index 137dfb484e0..1495aed6598 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -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 diff --git a/app/models/commit.rb b/app/models/commit.rb index 60a60f191d6..57069280ef7 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -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 diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml index a1f39e22e80..092834b993c 100644 --- a/app/views/admin/application_settings/network.html.haml +++ b/app/views/admin/application_settings/network.html.haml @@ -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' diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml index 180066723f1..aca9302aff7 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -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') diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml index 8e869fb4b71..a7da14d16ff 100644 --- a/app/views/admin/impersonation_tokens/index.html.haml +++ b/app/views/admin/impersonation_tokens/index.html.haml @@ -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 diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml index 4307060d748..aca8aa5d341 100644 --- a/app/views/ci/runner/_how_to_setup_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner.html.haml @@ -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', diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index 6750732ab67..8a1b7500abf 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -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') diff --git a/app/views/projects/_new_project_push_tip.html.haml b/app/views/projects/_new_project_push_tip.html.haml index 22e9522c0e7..e008130436c 100644 --- a/app/views/projects/_new_project_push_tip.html.haml +++ b/app/views/projects/_new_project_push_tip.html.haml @@ -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") diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml index 09f05b30433..abef33ca01c 100644 --- a/app/views/projects/buttons/_clone.html.haml +++ b/app/views/projects/buttons/_clone.html.haml @@ -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' diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 6c77036a85b..d07407a6d13 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -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') diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 0f913d11be1..3a9c7a8bec5 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -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) diff --git a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml index c805ee73acc..f295fa82192 100644 --- a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml +++ b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml @@ -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.") diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml index 928b54ea28f..57205682bda 100644 --- a/app/views/projects/merge_requests/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/_how_to_merge.html.haml @@ -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}" diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index 8a6e5fde99b..2f0394538bb 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -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 diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 7187afc7754..094cbf755e1 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -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")) diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index 9899cf9c6de..60de3630bb5 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -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 diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 1e509ea0d1f..cb834878276 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -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' diff --git a/app/views/shared/_personal_access_tokens_created_container.html.haml b/app/views/shared/_personal_access_tokens_created_container.html.haml index 42989b145a2..df4577e2862 100644 --- a/app/views/shared/_personal_access_tokens_created_container.html.haml +++ b/app/views/shared/_personal_access_tokens_created_container.html.haml @@ -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 diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 3b26b8df8a1..a55b7fc530a 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -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 diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index ced6af50501..22a6d5e33f0 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -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') diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 69481293f90..8d94a87a775 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -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 diff --git a/changelogs/unreleased/16654-audio-in-markdown.yml b/changelogs/unreleased/16654-audio-in-markdown.yml new file mode 100644 index 00000000000..8983e06da2c --- /dev/null +++ b/changelogs/unreleased/16654-audio-in-markdown.yml @@ -0,0 +1,5 @@ +--- +title: Feature enabling embedded audio elements in markdown. +merge_request: 17860 +author: Jesse Hall @jessehall3 +type: added diff --git a/changelogs/unreleased/27835-move-and-resize-panels-in-dashboard-save-to-branch.yml b/changelogs/unreleased/27835-move-and-resize-panels-in-dashboard-save-to-branch.yml new file mode 100644 index 00000000000..b7d7f37bccc --- /dev/null +++ b/changelogs/unreleased/27835-move-and-resize-panels-in-dashboard-save-to-branch.yml @@ -0,0 +1,5 @@ +--- +title: Add property to enable metrics dashboards to be rearranged +merge_request: 16605 +author: +type: changed diff --git a/changelogs/unreleased/33668-fix-search-term-xss.yml b/changelogs/unreleased/33668-fix-search-term-xss.yml new file mode 100644 index 00000000000..ed54542bd9c --- /dev/null +++ b/changelogs/unreleased/33668-fix-search-term-xss.yml @@ -0,0 +1,5 @@ +--- +title: HTML-escape search term in empty message +merge_request: 18319 +author: +type: security diff --git a/config/initializers/rack_attack_new.rb b/config/initializers/rack_attack_new.rb index 2812ceb3fd5..b0f7febe427 100644 --- a/config/initializers/rack_attack_new.rb +++ b/config/initializers/rack_attack_new.rb @@ -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') diff --git a/config/locales/sherlock.en.yml b/config/locales/sherlock.en.yml index f24b825f585..963e1d6295a 100644 --- a/config/locales/sherlock.en.yml +++ b/config/locales/sherlock.en.yml @@ -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: '%' diff --git a/db/migrate/20190930025655_add_incident_management_throttle_columns_to_application_setting.rb b/db/migrate/20190930025655_add_incident_management_throttle_columns_to_application_setting.rb new file mode 100644 index 00000000000..577c705fbef --- /dev/null +++ b/db/migrate/20190930025655_add_incident_management_throttle_columns_to_application_setting.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 81d19b9c8d0..3a1d5d73c89 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/doc/integration/saml.md b/doc/integration/saml.md index d0088eab957..b72be55aca3 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -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 diff --git a/doc/security/asset_proxy.md b/doc/security/asset_proxy.md index b480905339b..6e615028e8b 100644 --- a/doc/security/asset_proxy.md +++ b/doc/security/asset_proxy.md @@ -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: diff --git a/doc/user/img/markdown_audio.mp3 b/doc/user/img/markdown_audio.mp3 Binary files differnew file mode 100644 index 00000000000..8946c3b3b10 --- /dev/null +++ b/doc/user/img/markdown_audio.mp3 diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 475bd7d17a0..65ff176df27 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -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). diff --git a/doc/user/project/integrations/jira_cloud_configuration.md b/doc/user/project/integrations/jira_cloud_configuration.md index 1d5a4a3d4c7..9fa92f19e4f 100644 --- a/doc/user/project/integrations/jira_cloud_configuration.md +++ b/doc/user/project/integrations/jira_cloud_configuration.md @@ -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. diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md index 5313975908b..01f4eb5b912 100644 --- a/doc/user/project/issues/issue_data_and_actions.md +++ b/doc/user/project/issues/issue_data_and_actions.md @@ -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 diff --git a/doc/user/project/merge_requests/allow_collaboration.md b/doc/user/project/merge_requests/allow_collaboration.md index 3a389eb1e3a..083a117600b 100644 --- a/doc/user/project/merge_requests/allow_collaboration.md +++ b/doc/user/project/merge_requests/allow_collaboration.md @@ -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 diff --git a/doc/user/snippets.md b/doc/user/snippets.md index e55a407295e..77997c53210 100644 --- a/doc/user/snippets.md +++ b/doc/user/snippets.md @@ -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: diff --git a/lib/banzai/filter/audio_link_filter.rb b/lib/banzai/filter/audio_link_filter.rb new file mode 100644 index 00000000000..83aa520dc4b --- /dev/null +++ b/lib/banzai/filter/audio_link_filter.rb @@ -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 diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index df181406591..c7589e69262 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -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 diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb index 18947679b69..205f777bc90 100644 --- a/lib/banzai/filter/wiki_link_filter.rb +++ b/lib/banzai/filter/wiki_link_filter.rb @@ -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') diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index bb0d1eaa1e1..08e27257fdf 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -26,6 +26,7 @@ module Banzai Filter::ColorFilter, Filter::MermaidFilter, Filter::VideoLinkFilter, + Filter::AudioLinkFilter, Filter::ImageLazyLoadFilter, Filter::ImageLinkFilter, Filter::InlineMetricsFilter, diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 5a42952796c..ae29546cdac 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -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 diff --git a/lib/gitlab/file_markdown_link_builder.rb b/lib/gitlab/file_markdown_link_builder.rb index e9e5172e6f8..09d799b859d 100644 --- a/lib/gitlab/file_markdown_link_builder.rb +++ b/lib/gitlab/file_markdown_link_builder.rb @@ -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 diff --git a/lib/gitlab/file_type_detection.rb b/lib/gitlab/file_type_detection.rb index 7137720f204..ca78d49f99b 100644 --- a/lib/gitlab/file_type_detection.rb +++ b/lib/gitlab/file_type_detection.rb @@ -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 diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb index 275151f7fc1..560618bb486 100644 --- a/lib/gitlab/profiler.rb +++ b/lib/gitlab/profiler.rb @@ -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. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c41fa78f0c2..6fce25f2cb6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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" +msgstr "" + +msgid "Copy command" msgstr "" -msgid "Copy branch name to clipboard" +msgid "Copy commands" msgstr "" -msgid "Copy command to clipboard" +msgid "Copy commit SHA" msgstr "" -msgid "Copy commit SHA to clipboard" +msgid "Copy file contents" msgstr "" -msgid "Copy file path to clipboard" +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" diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb index 9f26321a1dc..84221f5555a 100644 --- a/spec/features/markdown/copy_as_gfm_spec.rb +++ b/spec/features/markdown/copy_as_gfm_spec.rb @@ -179,6 +179,12 @@ describe 'Copy as GFM', :js do ) verify( + 'AudioLinkFilter', + + '![Audio](https://example.com/audio.wav)' + ) + + verify( 'MathFilter: math as converted from GFM to HTML', '$`c = \pm\sqrt{a^2 + b^2}`$', diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb index 0efeffe3232..a45fa67ce9e 100644 --- a/spec/features/markdown/markdown_spec.rb +++ b/spec/features/markdown/markdown_spec.rb @@ -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 diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 18e2723ff8e..9dc0f7c90c2 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -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 diff --git a/spec/fixtures/audio_sample.wav b/spec/fixtures/audio_sample.wav Binary files differnew file mode 100644 index 00000000000..e0a42d58920 --- /dev/null +++ b/spec/fixtures/audio_sample.wav diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 8016cf7a86d..b19b45928d9 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -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` diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap index 0e04487cb20..08173f4f0c4 100644 --- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap +++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap @@ -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> diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js index f1943861523..d8c55bee8e0 100644 --- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js +++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js @@ -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', }, }); }); diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb index 303b84b23e6..e918c34ffef 100644 --- a/spec/helpers/button_helper_spec.rb +++ b/spec/helpers/button_helper_spec.rb @@ -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') diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index bc2a0bb7f38..9e9f87b3407 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -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 diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js index 4ac15ca5aa2..06c06613887 100644 --- a/spec/javascripts/blob/viewer/index_spec.js +++ b/spec/javascripts/blob/viewer/index_spec.js @@ -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(); }); diff --git a/spec/javascripts/monitoring/components/dashboard_spec.js b/spec/javascripts/monitoring/components/dashboard_spec.js index ab3ab477708..1b2b01d1c8c 100644 --- a/spec/javascripts/monitoring/components/dashboard_spec.js +++ b/spec/javascripts/monitoring/components/dashboard_spec.js @@ -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; diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js index fd17349d48f..29a76574b89 100644 --- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js +++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js @@ -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', }); diff --git a/spec/lib/banzai/filter/audio_link_filter_spec.rb b/spec/lib/banzai/filter/audio_link_filter_spec.rb new file mode 100644 index 00000000000..a8459137169 --- /dev/null +++ b/spec/lib/banzai/filter/audio_link_filter_spec.rb @@ -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 diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index f983265ce71..046c346a7ac 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -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' diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb index d2d539a62fc..4587bd85939 100644 --- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -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 diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 4a485fbc2bd..26f2b0b0acf 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -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 diff --git a/spec/lib/gitlab/file_markdown_link_builder_spec.rb b/spec/lib/gitlab/file_markdown_link_builder_spec.rb index d9e2e162ae8..de0ac9733e6 100644 --- a/spec/lib/gitlab/file_markdown_link_builder_spec.rb +++ b/spec/lib/gitlab/file_markdown_link_builder_spec.rb @@ -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 'my_image' + end + end + + context 'when file is video' do + let(:filename) { 'my_video.mp4' } + + it 'retrieves the name without the extension' do + expect(custom_class.markdown_name).to eq 'my_video' + end + end + + context 'when file is audio' do + let(:filename) { 'my_audio.wav' } it 'retrieves the name without the extension' do - expect(custom_class.markdown_name).to eq 'dk' + expect(custom_class.markdown_name).to eq 'my_audio' end end - context 'when file is not an image or video' do - let(:filename) { 'dk.zip' } + 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 diff --git a/spec/lib/gitlab/file_type_detection_spec.rb b/spec/lib/gitlab/file_type_detection_spec.rb index 1edf882afe2..05008bf895c 100644 --- a/spec/lib/gitlab/file_type_detection_spec.rb +++ b/spec/lib/gitlab/file_type_detection_spec.rb @@ -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 - end + it 'returns false if filename is a dangerous image extension' do + uploader.store!(upload_fixture('unsanitized.svg')) - def upload_fixture(filename) - fixture_file_upload(File.join('spec', 'fixtures', filename)) - end + expect(uploader).not_to be_dangerous_audio + end - describe '#image?' do - include_examples '#image? for an uploader' - end + it 'returns false for an image file' do + uploader.store!(upload_fixture('dk.png')) - describe '#video?' do - include_examples '#video? for an uploader' - end + expect(uploader).not_to be_dangerous_audio + end - describe '#image_or_video?' do - include_examples '#image? for an uploader' - include_examples '#video? for an uploader' - end + it 'returns false for an video file' do + uploader.store!(upload_fixture('video_sample.mp4')) - describe '#dangerous_image?' do - include_examples '#dangerous_image? for an uploader' - end + expect(uploader).not_to be_dangerous_audio + end - describe '#dangerous_video?' do - include_examples '#dangerous_video? for an uploader' + 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 - describe '#dangerous_image_or_video?' do - include_examples '#dangerous_image? for an uploader' - include_examples '#dangerous_video? for an uploader' + describe '#dangerous_embeddable?' do + it 'returns true if filename has a dangerous image extension' do + uploader.store!(upload_fixture('unsanitized.svg')) + + expect(uploader).to be_dangerous_embeddable + end + + it 'returns false for an image file' do + uploader.store!(upload_fixture('dk.png')) + + expect(uploader).not_to be_dangerous_embeddable + end + + it 'returns false for a video file' do + uploader.store!(upload_fixture('video_sample.mp4')) + + expect(uploader).not_to be_dangerous_embeddable + end + + 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 - end + it 'returns false for an image file' do + allow(custom_class).to receive(:filename).and_return('dk.png') - describe '#image?' do - include_examples '#image? for a regular class' - end + expect(custom_class).not_to be_dangerous_audio + end - describe '#video?' do - include_examples '#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 '#image_or_video?' do - include_examples '#image? for a regular class' - include_examples '#video? for a regular class' - end + expect(custom_class).not_to be_dangerous_audio + end - describe '#dangerous_image?' do - include_examples '#dangerous_image? for a regular class' - end + it 'returns false if file has a dangerous image extension' do + allow(custom_class).to receive(:filename).and_return('unsanitized.svg') - describe '#dangerous_video?' do - include_examples '#dangerous_video? for a regular class' + 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 '#dangerous_image_or_video?' do - include_examples '#dangerous_image? for a regular class' - include_examples '#dangerous_video? for a regular class' + 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') + + expect(custom_class).to be_dangerous_embeddable + 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_dangerous_embeddable + 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_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 diff --git a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb index 80b0935a7ed..dd379f2fe1f 100644 --- a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb +++ b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb @@ -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 } } } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 6e511c9e4ec..839c4cadb5e 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -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 diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb index 0e757e8743a..ca8720cd414 100644 --- a/spec/requests/rack_attack_global_spec.rb +++ b/spec/requests/rack_attack_global_spec.rb @@ -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 diff --git a/spec/support/helpers/rack_attack_spec_helpers.rb b/spec/support/helpers/rack_attack_spec_helpers.rb new file mode 100644 index 00000000000..234271ba1c0 --- /dev/null +++ b/spec/support/helpers/rack_attack_spec_helpers.rb @@ -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 diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 3274651ef19..323c8d1baf2 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -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', diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 3245f8418b1..35b2993443f 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -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 diff --git a/spec/support/shared_contexts/rack_attack_shared_context.rb b/spec/support/shared_contexts/rack_attack_shared_context.rb new file mode 100644 index 00000000000..c925f565226 --- /dev/null +++ b/spec/support/shared_contexts/rack_attack_shared_context.rb @@ -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 |