Skip to content
Snippets Groups Projects
Commit c8f773a8 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 929b887e
No related branches found
No related tags found
No related merge requests found
Showing
with 203 additions and 122 deletions
Please view this file on the master branch, on stable branches it's out of date.
 
## 12.7.3
- No changes.
## 12.7.1
 
### Fixed (1 change)
Loading
Loading
@@ -95,6 +99,14 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove "creations" in gitlab_subscription_histories on gitlab.com. !22278
 
 
## 12.6.6
- No changes.
## 12.6.5
- No changes.
## 12.6.4
 
- No changes.
Loading
Loading
@@ -207,6 +219,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Update the alerts used in the Dependency List to follow GitLab design guidelines. !21760
 
 
## 12.5.8
- No changes.
## 12.5.5
 
- No changes.
Loading
Loading
8.19.0
8.20.0
Loading
Loading
@@ -65,7 +65,7 @@ gem 'u2f', '~> 0.2.1'
 
# GitLab Pages
gem 'validates_hostname', '~> 1.0.6'
gem 'rubyzip', '~> 1.3.0', require: 'zip'
gem 'rubyzip', '~> 2.0.0', require: 'zip'
# GitLab Pages letsencrypt support
gem 'acme-client', '~> 2.0.5'
 
Loading
Loading
Loading
Loading
@@ -274,7 +274,7 @@ GEM
et-orbi (1.2.1)
tzinfo
eventmachine (1.2.7)
excon (0.62.0)
excon (0.71.1)
execjs (2.6.0)
expression_parser (0.9.0)
extended-markdown-filter (0.6.0)
Loading
Loading
@@ -961,7 +961,7 @@ GEM
sexp_processor (~> 4.9)
rubyntlm (0.6.2)
rubypants (0.2.0)
rubyzip (1.3.0)
rubyzip (2.0.0)
rugged (0.28.4.1)
safe_yaml (1.0.4)
sanitize (4.6.6)
Loading
Loading
@@ -1361,7 +1361,7 @@ DEPENDENCIES
ruby-prof (~> 1.0.0)
ruby-progressbar
ruby_parser (~> 3.8)
rubyzip (~> 1.3.0)
rubyzip (~> 2.0.0)
rugged (~> 0.28)
sanitize (~> 4.6)
sassc-rails (~> 2.1.0)
Loading
Loading
Loading
Loading
@@ -5,7 +5,7 @@ import AccessorUtilities from '~/lib/utils/accessor';
import eventHub from '../event_hub';
import store from '../store/';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
import { isMobile, updateExistingFrequentItem } from '../utils';
import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
import FrequentItemsSearchInput from './frequent_items_search_input.vue';
import FrequentItemsList from './frequent_items_list.vue';
import frequentItemsMixin from './frequent_items_mixin';
Loading
Loading
@@ -64,7 +64,9 @@ export default {
this.fetchFrequentItems();
}
},
logItemAccess(storageKey, item) {
logItemAccess(storageKey, unsanitizedItem) {
const item = sanitizeItem(unsanitizedItem);
if (!AccessorUtilities.isLocalStorageAccessSafe()) {
return false;
}
Loading
Loading
<script>
import FrequentItemsListItem from './frequent_items_list_item.vue';
import frequentItemsMixin from './frequent_items_mixin';
import { sanitizeItem } from '../utils';
 
export default {
components: {
Loading
Loading
@@ -48,6 +49,9 @@ export default {
? this.translations.itemListErrorMessage
: this.translations.itemListEmptyMessage;
},
sanitizedItems() {
return this.items.map(sanitizeItem);
},
},
};
</script>
Loading
Loading
@@ -59,7 +63,7 @@ export default {
{{ listEmptyMessage }}
</li>
<frequent-items-list-item
v-for="item in items"
v-for="item in sanitizedItems"
v-else
:key="item.id"
:item-id="item.id"
Loading
Loading
import _ from 'underscore';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import sanitize from 'sanitize-html';
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
 
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
Loading
Loading
@@ -43,3 +44,9 @@ export const updateExistingFrequentItem = (frequentItem, item) => {
lastAccessedOn: accessedOverHourAgo ? Date.now() : frequentItem.lastAccessedOn,
};
};
export const sanitizeItem = item => ({
...item,
name: sanitize(item.name.toString(), { allowedTags: [] }),
namespace: sanitize(item.namespace.toString(), { allowedTags: [] }),
});
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import Api from './api';
import { escape } from 'lodash';
import { normalizeHeaders } from './lib/utils/common_utils';
import { __ } from '~/locale';
 
Loading
Loading
@@ -75,10 +76,12 @@ const groupsSelect = () => {
}
},
formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
return `<div class='group-result'> <div class='group-name'>${escape(
object.full_name,
)}</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return object.full_name;
return escape(object.full_name);
},
dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results
Loading
Loading
Loading
Loading
@@ -14,7 +14,7 @@ import placeholderSystemNote from '../../vue_shared/components/notes/placeholder
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import { __ } from '~/locale';
import initUserPopovers from '../../user_popovers';
import initUserPopovers from '~/user_popovers';
 
export default {
name: 'NotesApp',
Loading
Loading
Loading
Loading
@@ -3,108 +3,92 @@ import Vue from 'vue';
import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
 
let renderedPopover;
let renderFn;
const handleUserPopoverMouseOut = event => {
const { target } = event;
target.removeEventListener('mouseleave', handleUserPopoverMouseOut);
if (renderFn) {
clearTimeout(renderFn);
}
if (renderedPopover) {
renderedPopover.$destroy();
renderedPopover = null;
}
target.removeAttribute('aria-describedby');
const removeTitle = el => {
// Removing titles so its not showing tooltips also
el.dataset.originalTitle = '';
el.setAttribute('title', '');
};
const getPreloadedUserInfo = dataset => {
const userId = dataset.user || dataset.userId;
const { username, name, avatarUrl } = dataset;
return {
userId,
username,
name,
avatarUrl,
};
};
 
/**
* Adds a UserPopover component to the body, hands over as much data as the target element has in data attributes.
* loads based on data-user-id more data about a user from the API and sets it on the popover
*/
const handleUserPopoverMouseOver = event => {
const { target } = event;
// Add listener to actually remove it again
target.addEventListener('mouseleave', handleUserPopoverMouseOut);
renderFn = setTimeout(() => {
// Helps us to use current markdown setup without maybe breaking or duplicating for now
if (target.dataset.user) {
target.dataset.userId = target.dataset.user;
// Removing titles so its not showing tooltips also
target.dataset.originalTitle = '';
target.setAttribute('title', '');
}
const { userId, username, name, avatarUrl } = target.dataset;
const populateUserInfo = user => {
const { userId } = user;
return Promise.all([UsersCache.retrieveById(userId), UsersCache.retrieveStatusById(userId)]).then(
([userData, status]) => {
if (userData) {
Object.assign(user, {
avatarUrl: userData.avatar_url,
username: userData.username,
name: userData.name,
location: userData.location,
bio: userData.bio,
organization: userData.organization,
loaded: true,
});
}
if (status) {
Object.assign(user, {
status,
});
}
return user;
},
);
};
export default (elements = document.querySelectorAll('.js-user-link')) => {
const userLinks = Array.from(elements);
return userLinks.map(el => {
const UserPopoverComponent = Vue.extend(UserPopover);
const user = {
userId,
username,
name,
avatarUrl,
location: null,
bio: null,
organization: null,
status: null,
loaded: false,
};
if (userId || username) {
const UserPopoverComponent = Vue.extend(UserPopover);
renderedPopover = new UserPopoverComponent({
propsData: {
target,
user,
},
});
renderedPopover.$mount();
UsersCache.retrieveById(userId)
.then(userData => {
if (!userData) {
return undefined;
}
Object.assign(user, {
avatarUrl: userData.avatar_url,
username: userData.username,
name: userData.name,
location: userData.location,
bio: userData.bio,
organization: userData.organization,
status: userData.status,
loaded: true,
});
if (userData.status) {
return Promise.resolve();
}
return UsersCache.retrieveStatusById(userId);
})
.then(status => {
if (!status) {
return;
}
Object.assign(user, {
status,
});
})
.catch(() => {
renderedPopover.$destroy();
renderedPopover = null;
});
}
}, 200); // 200ms delay so not every mouseover triggers Popover + API Call
};
const renderedPopover = new UserPopoverComponent({
propsData: {
target: el,
user,
},
});
renderedPopover.$mount();
el.addEventListener('mouseenter', ({ target }) => {
removeTitle(target);
const preloadedUserInfo = getPreloadedUserInfo(target.dataset);
Object.assign(user, preloadedUserInfo);
 
export default elements => {
const userLinks = elements || [...document.querySelectorAll('.js-user-link')];
if (preloadedUserInfo.userId) {
populateUserInfo(user);
}
});
el.addEventListener('mouseleave', ({ target }) => {
target.removeAttribute('aria-describedby');
});
 
userLinks.forEach(el => {
el.addEventListener('mouseenter', handleUserPopoverMouseOver);
return renderedPopover;
});
};
Loading
Loading
@@ -56,19 +56,16 @@ export default {
</script>
 
<template>
<gl-popover :target="target" boundary="viewport" placement="top" offset="0, 1" show>
<!-- 200ms delay so not every mouseover triggers Popover -->
<gl-popover :target="target" :delay="200" boundary="viewport" triggers="hover" placement="top">
<div class="user-popover d-flex">
<div class="p-1 flex-shrink-1">
<user-avatar-image :img-src="user.avatarUrl" :size="60" css-classes="mr-2" />
</div>
<div class="p-1 w-100">
<h5 class="m-0">
{{ user.name }}
<gl-skeleton-loading
v-if="nameIsLoading"
:lines="1"
class="animation-container-small mb-1"
/>
<span v-if="user.name">{{ user.name }}</span>
<gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" />
</h5>
<div class="text-secondary mb-2">
<span v-if="user.username">@{{ user.username }}</span>
Loading
Loading
Loading
Loading
@@ -5,6 +5,7 @@ require 'fogbugz'
 
class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include Gitlab::NoCacheHeaders
include GitlabRoutingHelper
include PageLayoutHelper
include SafeParamsHelper
Loading
Loading
@@ -55,7 +56,6 @@ class ApplicationController < ActionController::Base
# Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security
# concerns due to caching private data.
DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store"
DEFAULT_GITLAB_CONTROL_NO_CACHE = "#{DEFAULT_GITLAB_CACHE_CONTROL}, no-cache"
 
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
Loading
Loading
@@ -247,9 +247,9 @@ class ApplicationController < ActionController::Base
end
 
def no_cache_headers
headers['Cache-Control'] = DEFAULT_GITLAB_CONTROL_NO_CACHE
headers['Pragma'] = 'no-cache' # HTTP 1.0 compatibility
headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
DEFAULT_GITLAB_NO_CACHE_HEADERS.each do |k, v|
headers[k] = v
end
end
 
def default_headers
Loading
Loading
Loading
Loading
@@ -19,7 +19,7 @@ class DashboardController < Dashboard::ApplicationController
 
format.json do
load_events
pager_json("events/_events", @events.count)
pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) })
end
end
end
Loading
Loading
@@ -37,6 +37,7 @@ class DashboardController < Dashboard::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
.map(&:present)
 
Events::RenderService.new(current_user).execute(@events)
end
Loading
Loading
Loading
Loading
@@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController
 
format.json do
load_events
pager_json("events/_events", @events.count)
pager_json("events/_events", @events.count { |event| event.visible_to_user?(current_user) })
end
end
end
Loading
Loading
@@ -209,8 +209,9 @@ class GroupsController < Groups::ApplicationController
.includes(:namespace)
 
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups)
.to_a
.new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups)
.to_a
.map(&:present)
 
Events::RenderService
.new(current_user)
Loading
Loading
Loading
Loading
@@ -118,7 +118,7 @@ class ProjectsController < Projects::ApplicationController
format.html
format.json do
load_events
pager_json('events/_events', @events.count)
pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) })
end
end
end
Loading
Loading
@@ -343,6 +343,7 @@ class ProjectsController < Projects::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
.map(&:present)
 
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
Loading
Loading
Loading
Loading
@@ -10,6 +10,8 @@ module Types
@calls_gitaly = !!kwargs.delete(:calls_gitaly)
@constant_complexity = !!kwargs[:complexity]
kwargs[:complexity] ||= field_complexity(kwargs[:resolver_class])
@feature_flag = kwargs[:feature_flag]
kwargs = check_feature_flag(kwargs)
 
super(*args, **kwargs, &block)
end
Loading
Loading
@@ -28,8 +30,27 @@ module Types
@constant_complexity
end
 
def visible?(context)
return false if feature_flag.present? && !Feature.enabled?(feature_flag)
super
end
private
 
attr_reader :feature_flag
def feature_documentation_message(key, description)
"#{description}. Available only when feature flag #{key} is enabled."
end
def check_feature_flag(args)
args[:description] = feature_documentation_message(args[:feature_flag], args[:description]) if args[:feature_flag].present?
args.delete(:feature_flag)
args
end
def field_complexity(resolver_class)
if resolver_class
field_resolver_complexity
Loading
Loading
Loading
Loading
@@ -10,14 +10,19 @@ module Types
description: 'Internal ID of the Grafana integration'
field :grafana_url, GraphQL::STRING_TYPE, null: false,
description: 'Url for the Grafana host for the Grafana integration'
field :token, GraphQL::STRING_TYPE, null: false,
description: 'API token for the Grafana integration'
field :enabled, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates whether Grafana integration is enabled'
field :created_at, Types::TimeType, null: false,
description: 'Timestamp of the issue\'s creation'
field :updated_at, Types::TimeType, null: false,
description: 'Timestamp of the issue\'s last activity'
field :token, GraphQL::STRING_TYPE, null: false,
deprecation_reason: 'Plain text token has been masked for security reasons',
description: 'API token for the Grafana integration. Field is permanently masked.'
def token
object.masked_token
end
end
end
Loading
Loading
@@ -368,8 +368,8 @@ module ProjectsHelper
@project.grafana_integration&.grafana_url
end
 
def grafana_integration_token
@project.grafana_integration&.token
def grafana_integration_masked_token
@project.grafana_integration&.masked_token
end
 
def grafana_integration_enabled?
Loading
Loading
# frozen_string_literal: true
 
class GenericCommitStatus < CommitStatus
EXTERNAL_STAGE_IDX = 1_000_000
before_validation :set_default_values
 
validates :target_url, addressable_url: true,
length: { maximum: 255 },
allow_nil: true
validate :name_uniqueness_across_types, unless: :importing?
 
# GitHub compatible API
alias_attribute :context, :name
Loading
Loading
@@ -13,7 +16,7 @@ class GenericCommitStatus < CommitStatus
def set_default_values
self.context ||= 'default'
self.stage ||= 'external'
self.stage_idx ||= 1000000
self.stage_idx ||= EXTERNAL_STAGE_IDX
end
 
def tags
Loading
Loading
@@ -25,4 +28,14 @@ class GenericCommitStatus < CommitStatus
.new(self, current_user)
.fabricate!
end
private
def name_uniqueness_across_types
return if !pipeline || name.blank?
if pipeline.statuses.by_name(name).where.not(type: type).exists?
errors.add(:name, :taken)
end
end
end
Loading
Loading
@@ -8,11 +8,13 @@ class GrafanaIntegration < ApplicationRecord
algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32
 
before_validation :check_token_changes
validates :grafana_url,
length: { maximum: 1024 },
addressable_url: { enforce_sanitization: true, ascii_only: true }
 
validates :token, :project, presence: true
validates :encrypted_token, :project, presence: true
 
validates :enabled, inclusion: { in: [true, false] }
 
Loading
Loading
@@ -23,4 +25,28 @@ class GrafanaIntegration < ApplicationRecord
 
@client ||= ::Grafana::Client.new(api_url: grafana_url.chomp('/'), token: token)
end
def masked_token
mask(encrypted_token)
end
def masked_token_was
mask(encrypted_token_was)
end
private
def token
decrypt(:token, encrypted_token)
end
def check_token_changes
return unless [encrypted_token_was, masked_token_was].include?(token)
clear_attribute_changes [:token, :encrypted_token, :encrypted_token_iv]
end
def mask(token)
token&.squish&.gsub(/./, '*')
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment