Commit 477c2c26 authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 4be2167e
......@@ -2,18 +2,6 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 13.3.5 (2020-09-04)
### Fixed (6 changes)
- Coerce string object storage options to booleans. !39901
- Fix Jira importer user mapping limit. !40310
- Fix auto-deploy-image external chart dependencies. !40730
- Fix ActiveRecord::IrreversibleOrderError during restore from backup. !40789
- Fix wrong caching logic in ProcessRefChangesService. !40821
- Update the 2FA user update check to account for rounding errors. !41327
## 13.3.4 (2020-09-02)
### Security (1 change)
......@@ -601,14 +589,6 @@ entry.
- Replace fa-pencil icon with GitLab SVG. !39648
## 13.2.9 (2020-09-04)
### Fixed (2 changes)
- Fix ActiveRecord::IrreversibleOrderError during restore from backup. !40789
- Update the 2FA user update check to account for rounding errors. !41327
## 13.2.8 (2020-09-02)
### Security (1 change)
......
......@@ -237,7 +237,7 @@ export default class Clusters {
}
addBannerCloseHandler(el, status) {
el.querySelector('.js-close-banner').addEventListener('click', () => {
el.querySelector('.js-close').addEventListener('click', () => {
el.classList.add('hidden');
this.setBannerDismissedState(status, true);
});
......
import { isEmpty } from 'lodash';
import { queryToObject } from '~/lib/utils/url_utility';
/**
* Strips enclosing quotations from a string if it has one.
*
......@@ -29,3 +32,133 @@ export const uniqueTokens = tokens => {
return uniques;
}, []);
};
/**
* Creates a token from a type and a filter. Example returned object
* { type: 'myType', value: { data: 'myData', operator: '= '} }
* @param {String} type the name of the filter
* @param {Object}
* @param {Object.value} filter value to be returned as token data
* @param {Object.operator} filter operator to be retuned as token operator
* @return {Object}
* @return {Object.type} token type
* @return {Object.value} token value
*/
function createToken(type, filter) {
return { type, value: { data: filter.value, operator: filter.operator } };
}
/**
* This function takes a filter object and translates it into a token array
* @param {Object} filters
* @param {Object.myFilterName} a single filter value or an array of filters
* @return {Array} tokens an array of tokens created from filter values
*/
export function prepareTokens(filters = {}) {
return Object.keys(filters).reduce((memo, key) => {
const value = filters[key];
if (!value) {
return memo;
}
if (Array.isArray(value)) {
return [...memo, ...value.map(filterValue => createToken(key, filterValue))];
}
return [...memo, createToken(key, value)];
}, []);
}
export function processFilters(filters) {
return filters.reduce((acc, token) => {
const { type, value } = token;
const { operator } = value;
const tokenValue = value.data;
if (!acc[type]) {
acc[type] = [];
}
acc[type].push({ value: tokenValue, operator });
return acc;
}, {});
}
/**
* This function takes a filter object and maps it into a query object. Example filter:
* { myFilterName: { value: 'foo', operator: '=' } }
* gets translated into:
* { myFilterName: 'foo', 'not[myFilterName]': null }
* @param {Object} filters
* @param {Object.myFilterName} a single filter value or an array of filters
* @return {Object} query object with both filter name and not-name with values
*/
export function filterToQueryObject(filters = {}) {
return Object.keys(filters).reduce((memo, key) => {
const filter = filters[key];
let selected;
let unselected;
if (Array.isArray(filter)) {
selected = filter.filter(item => item.operator === '=').map(item => item.value);
unselected = filter.filter(item => item.operator === '!=').map(item => item.value);
} else {
selected = filter?.operator === '=' ? filter.value : null;
unselected = filter?.operator === '!=' ? filter.value : null;
}
if (isEmpty(selected)) {
selected = null;
}
if (isEmpty(unselected)) {
unselected = null;
}
return { ...memo, [key]: selected, [`not[${key}]`]: unselected };
}, {});
}
/**
* Extracts filter name from url name, e.g. `not[my_filter]` => `my_filter`
* and returns the operator with it depending on the filter name
* @param {String} filterName from url
* @return {Object}
* @return {Object.filterName} extracted filtern ame
* @return {Object.operator} `=` or `!=`
*/
function extractNameAndOperator(filterName) {
// eslint-disable-next-line @gitlab/require-i18n-strings
if (filterName.startsWith('not[') && filterName.endsWith(']')) {
return { filterName: filterName.slice(4, -1), operator: '!=' };
}
return { filterName, operator: '=' };
}
/**
* This function takes a URL query string and maps it into a filter object. Example query string:
* '?myFilterName=foo'
* gets translated into:
* { myFilterName: { value: 'foo', operator: '=' } }
* @param {String} query URL quert string, e.g. from `window.location.search`
* @return {Object} filter object with filter names and their values
*/
export function urlQueryToFilter(query = '') {
const filters = queryToObject(query, { gatherArrays: true });
return Object.keys(filters).reduce((memo, key) => {
const value = filters[key];
if (!value) {
return memo;
}
const { filterName, operator } = extractNameAndOperator(key);
let previousValues = [];
if (Array.isArray(memo[filterName])) {
previousValues = memo[filterName];
}
if (Array.isArray(value)) {
const newAdditions = value.filter(Boolean).map(item => ({ value: item, operator }));
return { ...memo, [filterName]: [...previousValues, ...newAdditions] };
}
return { ...memo, [filterName]: { value, operator } };
}, {});
}
......@@ -29,6 +29,7 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
const emphasisNode = 'EM, I';
const strongNode = 'STRONG, B';
const headingNode = 'H1, H2, H3, H4, H5, H6';
const preCodeNode = 'PRE CODE';
return {
TEXT_NODE(node) {
......@@ -91,6 +92,13 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
return attributeDefinition ? `${result.trimRight()}\n${attributeDefinition}\n\n` : result;
},
[preCodeNode](node, subContent) {
const isReferenceDefinition = Boolean(node.dataset.sseReferenceDefinition);
return isReferenceDefinition
? `\n\n${node.innerText}\n`
: baseRenderer.convert(node, subContent);
},
};
};
......
import { renderUneditableBranch as render } from './render_utils';
const identifierRegex = /(^\[.+\]: .+)/;
const isIdentifier = text => {
......@@ -10,4 +8,33 @@ const canRender = (node, context) => {
return isIdentifier(context.getChildrenText(node));
};
const getReferenceDefinitions = (node, definitions = '') => {
if (!node) {
return definitions;
}
const definition = node.type === 'text' ? node.literal : '\n';
return getReferenceDefinitions(node.next, `${definitions}${definition}`);
};
const render = (node, { skipChildren }) => {
const content = getReferenceDefinitions(node.firstChild);
skipChildren();
return [
{
type: 'openTag',
tagName: 'pre',
classNames: ['code-block', 'language-markdown'],
attributes: { 'data-sse-reference-definition': true },
},
{ type: 'openTag', tagName: 'code' },
{ type: 'text', content },
{ type: 'closeTag', tagName: 'code' },
{ type: 'closeTag', tagName: 'pre' },
];
};
export default { canRender, render };
......@@ -149,7 +149,7 @@ def update
password_confirmation: params[:user][:password_confirmation]
}
password_params[:password_expires_at] = Time.current unless changing_own_password?
password_params[:password_expires_at] = Time.current if admin_making_changes_for_another_user?
user_params_with_pass.merge!(password_params)
end
......@@ -157,6 +157,7 @@ def update
respond_to do |format|
result = Users::UpdateService.new(current_user, user_params_with_pass.merge(user: user)).execute do |user|
user.skip_reconfirmation!
user.send_only_admin_changed_your_password_notification! if admin_making_changes_for_another_user?
end
if result[:status] == :success
......@@ -197,8 +198,8 @@ def remove_email
protected
def changing_own_password?
user == current_user
def admin_making_changes_for_another_user?
user != current_user
end
def user
......
# frozen_string_literal: true
class Profiles::NotificationsController < Profiles::ApplicationController
NOTIFICATIONS_PER_PAGE = 10
# rubocop: disable CodeReuse/ActiveRecord
def show
@user = current_user
@user_groups = user_groups
@group_notifications = user_groups.map { |group| current_user.notification_settings_for(group, inherit: true) }
@group_notifications = UserGroupNotificationSettingsFinder.new(current_user, user_groups).execute
@project_notifications = current_user.notification_settings.for_projects.order(:id)
.preload_source_route
......@@ -35,6 +33,6 @@ def user_params
private
def user_groups
GroupsFinder.new(current_user, all_available: false).execute.order_name_asc.page(params[:page]).per(NOTIFICATIONS_PER_PAGE)
GroupsFinder.new(current_user, all_available: false).execute.order_name_asc.page(params[:page])
end
end
# frozen_string_literal: true
class UserGroupNotificationSettingsFinder
def initialize(user, groups)
@user = user
@groups = groups
end
def execute
groups_with_ancestors = Gitlab::ObjectHierarchy.new(groups).base_and_ancestors
@loaded_groups_with_ancestors = groups_with_ancestors.index_by(&:id)
@loaded_notification_settings = user.notification_settings_for_groups(groups_with_ancestors).preload_source_route.index_by(&:source_id)
groups.map do |group|
find_notification_setting_for(group)
end
end
private
attr_reader :user, :groups, :loaded_groups_with_ancestors, :loaded_notification_settings
def find_notification_setting_for(group)
return loaded_notification_settings[group.id] if loaded_notification_settings[group.id]
return user.notification_settings.build(source: group) if group.parent_id.nil?
parent_setting = loaded_notification_settings[group.parent_id]
if should_copy?(parent_setting)
user.notification_settings.build(source: group) do |ns|
ns.assign_attributes(parent_setting.slice(*NotificationSetting.allowed_fields))
end
else
find_notification_setting_for(loaded_groups_with_ancestors[group.parent_id])
end
end
def should_copy?(parent_setting)
return false unless parent_setting
parent_setting.level != NotificationSetting.levels[:global] || parent_setting.notification_email.present?
end
end
......@@ -46,7 +46,7 @@ def node_selection
if lookahead.selects?(:nodes)
lookahead.selection(:nodes)
elsif lookahead.selects?(:edges)
lookahead.selection(:edges).selection(:nodes)
lookahead.selection(:edges).selection(:node)
end
end
end
......@@ -34,7 +34,8 @@ def continue_issue_resolve(parent, finder, **args)
def preloads
{
alert_management_alert: [:alert_management_alert]
alert_management_alert: [:alert_management_alert],
labels: [:labels]
}
end
......
......@@ -42,8 +42,7 @@ class IssueType < BaseObject
field :assignees, Types::UserType.connection_type, null: true, complexity: 5,
description: 'Assignees of the issue'
# Remove complexity when BatchLoader is used
field :labels, Types::LabelType.connection_type, null: true, complexity: 5,
field :labels, Types::LabelType.connection_type, null: true,
description: 'Labels of the issue'
field :milestone, Types::MilestoneType, null: true,
description: 'Milestone of the issue',
......
......@@ -181,6 +181,10 @@ def say_hi(user)
_('Hi %{username}!') % { username: sanitize_name(user.name) }
end
def say_hello(user)
_('Hello, %{username}!') % { username: sanitize_name(user.name) }
end
def two_factor_authentication_disabled_text
_('Two-factor authentication has been disabled for your GitLab account.')
end
......@@ -190,7 +194,7 @@ def re_enable_two_factor_authentication_text(format: nil)
case format
when :html
settings_link_to = link_to(_('two-factor authentication settings'), url, target: :_blank, rel: 'noopener noreferrer').html_safe
settings_link_to = generate_link(_('two-factor authentication settings'), url).html_safe
_("If you want to re-enable two-factor authentication, visit the %{settings_link_to} page.").html_safe % { settings_link_to: settings_link_to }
else
_('If you want to re-enable two-factor authentication, visit %{two_factor_link}') %
......@@ -198,8 +202,28 @@ def re_enable_two_factor_authentication_text(format: nil)
end
end
def admin_changed_password_text(format: nil)
url = Gitlab.config.gitlab.url
case format
when :html
link_to = generate_link(url, url).html_safe
_('An administrator changed the password for your GitLab account on %{link_to}.').html_safe % { link_to: link_to }
else
_('An administrator changed the password for your GitLab account on %{link_to}.') % { link_to: url }
end
end
def contact_your_administrator_text
_('Please contact your administrator with any questions.')
end
private
def generate_link(text, url)
link_to(text, url, target: :_blank, rel: 'noopener noreferrer')
end
def show_footer?
email_header_and_footer_enabled? && current_appearance&.show_footer?
end
......
......@@ -9,6 +9,10 @@ class DeviseMailer < Devise::Mailer
helper EmailsHelper
helper ApplicationHelper
def password_change_by_admin(record, opts = {})
devise_mail(record, :password_change_by_admin, opts)
end
protected
def subject_for(key)
......
# frozen_string_literal: true
module AdminChangedPasswordNotifier
# This module is responsible for triggering the `Password changed by administrator` emails
# when a GitLab administrator changes the password of another user.
# Usage
# These emails are disabled by default and are never trigerred after updating the password, unless
# explicitly specified.
# To explicitly trigger this email, the `send_only_admin_changed_your_password_notification!`
# method should be called, so like:
# user = User.find_by(email: 'hello@example.com')
# user.send_only_admin_changed_your_password_notification!
# user.password = user.password_confirmation = 'new_password'
# user.save!
# The `send_only_admin_changed_your_password_notification` has 2 responsibilities.
# It prevents triggering Devise's default `Password changed` email.
# It trigggers the `Password changed by administrator` email.
# It is important to skip sending the default Devise email when sending out `Password changed by administrator`
# email because we should not be sending 2 emails for the same event,
# hence the only public API made available from this module is `send_only_admin_changed_your_password_notification!`
# There is no public API made available to send the `Password changed by administrator` email,
# *without* skipping the default `Password changed` email, to prevent the problem mentioned above.
extend ActiveSupport::Concern
included do
after_update :send_admin_changed_your_password_notification, if: :send_admin_changed_your_password_notification?
end
def initialize(*args, &block)
@allow_admin_changed_your_password_notification = false # These emails are off by default
super
end
def send_only_admin_changed_your_password_notification!
skip_password_change_notification! # skip sending the default Devise 'password changed' notification
allow_admin_changed_your_password_notification!
end
private
def send_admin_changed_your_password_notification
send_devise_notification(:password_change_by_admin)
end
def allow_admin_changed_your_password_notification!
@allow_admin_changed_your_password_notification = true # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def send_admin_changed_your_password_notification?
self.class.send_password_change_notification && saved_change_to_encrypted_password? &&
@allow_admin_changed_your_password_notification # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
......@@ -73,7 +73,8 @@ def most_recent
enum issue_type: {
issue: 0,
incident: 1
incident: 1,
test_case: 2 ## EE-only
}
alias_attribute :parent_ids, :project_id
......
......@@ -58,6 +58,8 @@ class User < ApplicationRecord
devise :lockable, :recoverable, :rememberable, :trackable,
:validatable, :omniauthable, :confirmable, :registerable
include AdminChangedPasswordNotifier
# This module adds async behaviour to Devise emails
# and should be added after Devise modules are initialized.
include AsyncDeviseEmail
......@@ -1461,6 +1463,11 @@ def notification_settings_for(source, inherit: false)
end
end
def notification_settings_for_groups(groups)
ids = groups.is_a?(ActiveRecord::Relation) ? groups.select(:id) : groups.map(&:id)
notification_settings.for_groups.where(source_id: ids)
end
# Lazy load global notification setting
# Initializes User setting with Participating level if setting not persisted
def global_notification_setting
......
......@@ -60,6 +60,21 @@ def available_for?(merge_request)
end
end
##
# NOTE: This method is to be removed when `disallow_to_create_merge_request_pipelines_in_target_project`
# feature flag is removed.
def self.can_add_to_merge_train?(merge_request)
if Gitlab::Ci::Features.disallow_to_create_merge_request_pipelines_in_target_project?(merge_request.target_project)
merge_request.for_same_project?
else
true
end
end
def can_add_to_merge_train?(merge_request)
self.class.can_add_to_merge_train?(merge_request)
end
private
# Overridden in child classes
......
......@@ -48,12 +48,18 @@ def pipeline_ref_for_detached_merge_request_pipeline(m