Commit cab8cd7e authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge commit '981b5905' into...

Merge commit '981b5905' into backstage/gb/build-stages-catch-up-migration

* commit '981b5905': (40 commits)
parents 011ddb51 981b5905
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.5.1 (2018-02-22)
- No changes.
## 10.5.0 (2018-02-22) ## 10.5.0 (2018-02-22)
### Security (3 changes, 1 of them is from the community) ### Security (3 changes, 1 of them is from the community)
......
...@@ -9,7 +9,7 @@ const Api = { ...@@ -9,7 +9,7 @@ const Api = {
projectsPath: '/api/:version/projects.json', projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id', projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels', projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels', groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key', licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key', gitignorePath: '/api/:version/templates/gitignores/:key',
gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
...@@ -32,7 +32,7 @@ const Api = { ...@@ -32,7 +32,7 @@ const Api = {
}, },
// Return groups list. Filtered by query // Return groups list. Filtered by query
groups(query, options, callback) { groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath); const url = Api.buildUrl(Api.groupsPath);
return axios.get(url, { return axios.get(url, {
params: Object.assign({ params: Object.assign({
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
mixins: [ mixins: [
pipelinesMixin, pipelinesMixin,
], ],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
> >
<div class="item-details"> <div class="item-details">
<!-- FIXME: Pass an alt attribute here for accessibility --> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> <user-avatar-image :img-src="mergeRequest.author.avatarUrl" />
<h5 class="item-title merge-merquest-title"> <h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url"> <a :href="mergeRequest.url">
{{ mergeRequest.title }} {{ mergeRequest.title }}
......
...@@ -71,7 +71,7 @@ export default () => { ...@@ -71,7 +71,7 @@ export default () => {
el: '#resolve-count-app', el: '#resolve-count-app',
components: { components: {
'resolve-count': ResolveCount 'resolve-count': ResolveCount
} },
}); });
$(window).trigger('resize.nav'); $(window).trigger('resize.nav');
......
...@@ -61,111 +61,11 @@ var Dispatcher; ...@@ -61,111 +61,11 @@ var Dispatcher;
case 'projects:find_file:show': case 'projects:find_file:show':
case 'projects:blob:show': case 'projects:blob:show':
case 'projects:blame:show': case 'projects:blame:show':
shortcut_handler = true;
break;
case 'groups:labels:new':
import('./pages/groups/labels/new')
.then(callDefault)
.catch(fail);
break;
case 'groups:labels:edit':
import('./pages/groups/labels/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:labels:new':
import('./pages/projects/labels/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:labels:edit':
import('./pages/projects/labels/edit')
.then(callDefault)
.catch(fail);
break;
case 'groups:labels:index':
import('./pages/groups/labels/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:labels:index':
import('./pages/projects/labels/index')
.then(callDefault)
.catch(fail);
break;
case 'projects:network:show': case 'projects:network:show':
// Ensure we don't create a particular shortcut handler here. This is
// already created, where the network graph is created.
shortcut_handler = true;
break;
case 'projects:forks:new':
import('./pages/projects/forks/new')
.then(callDefault)
.catch(fail);
break;
case 'projects:artifacts:browse': case 'projects:artifacts:browse':
import('./pages/projects/artifacts/browse')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:artifacts:file': case 'projects:artifacts:file':
import('./pages/projects/artifacts/file')
.then(callDefault)
.catch(fail);
shortcut_handler = true; shortcut_handler = true;
break; break;
case 'search:show':
import('./pages/search/show')
.then(callDefault)
.catch(fail);
break;
case 'projects:settings:repository:show':
import('./pages/projects/settings/repository/show')
.then(callDefault)
.catch(fail);
break;
case 'projects:settings:ci_cd:show':
import('./pages/projects/settings/ci_cd/show')
.then(callDefault)
.catch(fail);
break;
case 'groups:settings:ci_cd:show':
import('./pages/groups/settings/ci_cd/show')
.then(callDefault)
.catch(fail);
break;
case 'ci:lints:create':
case 'ci:lints:show':
import('./pages/ci/lints')
.then(callDefault)
.catch(fail);
break;
case 'admin:conversational_development_index:show':
import('./pages/admin/conversational_development_index/show')
.then(callDefault)
.catch(fail);
break;
case 'import:fogbugz:new_user_map':
import('./pages/import/fogbugz/new_user_map')
.then(callDefault)
.catch(fail);
break;
case 'profiles:personal_access_tokens:index':
import('./pages/profiles/personal_access_tokens')
.then(callDefault)
.catch(fail);
break;
case 'admin:impersonation_tokens:index':
import('./pages/admin/impersonation_tokens')
.then(callDefault)
.catch(fail);
break;
case 'dashboard:groups:index':
import('./pages/dashboard/groups/index')
.then(callDefault)
.catch(fail);
break;
} }
switch (path[0]) { switch (path[0]) {
case 'admin': case 'admin':
......
...@@ -111,6 +111,9 @@ export default class FilteredSearchDropdown { ...@@ -111,6 +111,9 @@ export default class FilteredSearchDropdown {
if (hook) { if (hook) {
const data = hook.list.data || []; const data = hook.list.data || [];
if (!data) return;
const results = data.map((o) => { const results = data.map((o) => {
const updated = o; const updated = o;
updated.droplab_hidden = false; updated.droplab_hidden = false;
......
...@@ -607,7 +607,20 @@ GitLabDropdown = (function() { ...@@ -607,7 +607,20 @@ GitLabDropdown = (function() {
}; };
GitLabDropdown.prototype.renderItem = function(data, group, index) { GitLabDropdown.prototype.renderItem = function(data, group, index) {
var field, fieldName, html, selected, text, url, value; var field, fieldName, html, selected, text, url, value, rowHidden;
if (!this.options.renderRow) {
value = this.options.id ? this.options.id(data) : data.id;
if (value) {
value = value.toString().replace(/'/g, '\\\'');
}
}
// Hide element
if (this.options.hideRow && this.options.hideRow(value)) {
rowHidden = true;
}
if (group == null) { if (group == null) {
group = false; group = false;
} }
...@@ -616,6 +629,7 @@ GitLabDropdown = (function() { ...@@ -616,6 +629,7 @@ GitLabDropdown = (function() {
index = false; index = false;
} }
html = document.createElement('li'); html = document.createElement('li');
if (data === 'divider' || data === 'separator') { if (data === 'divider' || data === 'separator') {
html.className = data; html.className = data;
return html; return html;
...@@ -631,11 +645,9 @@ GitLabDropdown = (function() { ...@@ -631,11 +645,9 @@ GitLabDropdown = (function() {
html = this.options.renderRow.call(this.options, data, this); html = this.options.renderRow.call(this.options, data, this);
} else { } else {
if (!selected) { if (!selected) {
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName; fieldName = this.options.fieldName;
if (value) { if (value) {
value = value.toString().replace(/'/g, '\\\'');
field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`); field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
if (field.length) { if (field.length) {
selected = true; selected = true;
......
...@@ -30,11 +30,11 @@ ...@@ -30,11 +30,11 @@
default: 'bottom', default: 'bottom',
}, },
/** /**
* value could either be number or string * value could either be number or string
* as `memberCount` is always passed as string * as `memberCount` is always passed as string
* while `subgroupCount` & `projectCount` * while `subgroupCount` & `projectCount`
* are always number * are always number
*/ */
value: { value: {
type: [Number, String], type: [Number, String],
required: false, required: false,
......
...@@ -316,9 +316,9 @@ export default class LabelsSelect { ...@@ -316,9 +316,9 @@ export default class LabelsSelect {
}, },
multiSelect: $dropdown.hasClass('js-multiselect'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(options) { clicked: function (clickEvent) {
const { $el, e, isMarking } = options; const { $el, e, isMarking } = clickEvent;
const label = options.selectedObj; const label = clickEvent.selectedObj;
var isIssueIndex, isMRIndex, page, boardsModel; var isIssueIndex, isMRIndex, page, boardsModel;
var fadeOutLoader = () => { var fadeOutLoader = () => {
......
...@@ -418,6 +418,16 @@ export const convertObjectPropsToCamelCase = (obj = {}) => { ...@@ -418,6 +418,16 @@ export const convertObjectPropsToCamelCase = (obj = {}) => {
export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`; export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
$(selector).on('focusin', function selectOnFocusCallback() {
$(this).select().one('mouseup', (e) => {
e.preventDefault();
});
});
};
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.utils = { window.gl.utils = {
...(window.gl.utils || {}), ...(window.gl.utils || {}),
......
...@@ -10,7 +10,7 @@ window.jQuery = jQuery; ...@@ -10,7 +10,7 @@ window.jQuery = jQuery;
window.$ = jQuery; window.$ = jQuery;
// lib/utils // lib/utils
import { handleLocationHash } from './lib/utils/common_utils'; import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility';
...@@ -104,13 +104,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -104,13 +104,7 @@ document.addEventListener('DOMContentLoaded', () => {
return true; return true;
}); });
// Click a .js-select-on-focus field, select the contents addSelectOnFocusBehaviour('.js-select-on-focus');
// Prevent a mouseup event from deselecting the input
$('.js-select-on-focus').on('focusin', function selectOnFocusCallback() {
$(this).select().one('mouseup', (e) => {
e.preventDefault();
});
});
$('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() {
$(this).tooltip('destroy') $(this).tooltip('destroy')
......
import UserCallout from '../../../../user_callout'; import UserCallout from '~/user_callout';
export default () => new UserCallout(); document.addEventListener('DOMContentLoaded', () => new UserCallout());
import DueDateSelectors from '../../../due_date_select'; import DueDateSelectors from '~/due_date_select';
export default () => new DueDateSelectors(); document.addEventListener('DOMContentLoaded', () => new DueDateSelectors());
import CILintEditor from '../ci_lint_editor';
document.addEventListener('DOMContentLoaded', () => new CILintEditor());
import CILintEditor from './ci_lint_editor';
export default () => new CILintEditor();
import CILintEditor from '../ci_lint_editor';
document.addEventListener('DOMContentLoaded', () => new CILintEditor());
import initGroupsList from '~/groups'; import initGroupsList from '~/groups';
export default initGroupsList; document.addEventListener('DOMContentLoaded', initGroupsList);
import Labels from '~/labels'; import Labels from '~/labels';
export default () => new Labels(); document.addEventListener('DOMContentLoaded', () => new Labels());
import initLabels from '~/init_labels'; import initLabels from '~/init_labels';
export default initLabels; document.addEventListener('DOMContentLoaded', initLabels);
import Labels from '~/labels'; import Labels from '~/labels';
export default () => new Labels(); document.addEventListener('DOMContentLoaded', () => new Labels());
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default () => { document.addEventListener('DOMContentLoaded', () => {
const variableListEl = document.querySelector('.js-ci-variable-list-section'); const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new AjaxVariableList({ new AjaxVariableList({
...@@ -9,4 +9,4 @@ export default () => { ...@@ -9,4 +9,4 @@ export default () => {
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint, saveEndpoint: variableListEl.dataset.saveEndpoint,
}); });
}; });
import UsersSelect from '../../../../users_select'; import UsersSelect from '~/users_select';
export default () => new UsersSelect(); document.addEventListener('DOMContentLoaded', () => new UsersSelect());
import DueDateSelectors from '../../../due_date_select'; import DueDateSelectors from '~/due_date_select';
export default () => new DueDateSelectors(); document.addEventListener('DOMContentLoaded', () => new DueDateSelectors());
import BuildArtifacts from '~/build_artifacts'; import BuildArtifacts from '~/build_artifacts';
import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsNavigation from '~/shortcuts_navigation';
export default function () { document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
new BuildArtifacts(); // eslint-disable-line no-new new BuildArtifacts(); // eslint-disable-line no-new
} });
import BlobViewer from '~/blob/viewer/index'; import BlobViewer from '~/blob/viewer/index';
import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsNavigation from '~/shortcuts_navigation';
export default function () { document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
} });
import ProjectFork from '~/project_fork'; import ProjectFork from '~/project_fork';
export default () => { document.addEventListener('DOMContentLoaded', () => new ProjectFork());
new ProjectFork(); // eslint-disable-line no-new
};
import Labels from '~/labels'; import Labels from '~/labels';
export default () => new Labels(); document.addEventListener('DOMContentLoaded', () => new Labels());
import initLabels from '~/init_labels'; import initLabels from '~/init_labels';
export default initLabels; document.addEventListener('DOMContentLoaded', initLabels);
import Labels from '~/labels'; import Labels from '~/labels';
export default () => new Labels(); document.addEventListener('DOMContentLoaded', () => new Labels());
...@@ -50,7 +50,7 @@ export default class Project { ...@@ -50,7 +50,7 @@ export default class Project {
Project.projectSelectDropdown(); Project.projectSelectDropdown();
} }
static projectSelectDropdown () { static projectSelectDropdown() {
projectSelect(); projectSelect();
$('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
} }
......
...@@ -2,7 +2,7 @@ import initSettingsPanels from '~/settings_panels'; ...@@ -2,7 +2,7 @@ import initSettingsPanels from '~/settings_panels';
import SecretValues from '~/behaviors/secret_values'; import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
export default function () { document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
...@@ -22,4 +22,4 @@ export default function () { ...@@ -22,4 +22,4 @@ export default function () {
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint, saveEndpoint: variableListEl.dataset.saveEndpoint,
}); });
} });
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
export default initSettingsPanels; document.addEventListener('DOMContentLoaded', initSettingsPanels);
import Search from './search'; import Search from './search';
export default () => new Search(); document.addEventListener('DOMContentLoaded', () => new Search());
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
jobComponent, jobComponent,
dropdownJobComponent, dropdownJobComponent,
}, },
props: { props: {
title: { title: {
type: String, type: String,
......
...@@ -223,7 +223,8 @@ ...@@ -223,7 +223,8 @@
<div class="table-section section-10 commit-link"> <div class="table-section section-10 commit-link">
<div <div
class="table-mobile-header" class="table-mobile-header"
role="rowheader"> role="rowheader"
>
Status Status
</div> </div>
<div class="table-mobile-content"> <div class="table-mobile-content">
......
...@@ -50,9 +50,7 @@ ...@@ -50,9 +50,7 @@
computed: { computed: {
dropdownClass() { dropdownClass() {
return this.dropdownContent.length > 0 ? return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
'js-builds-dropdown-container' :
'js-builds-dropdown-loading';
}, },
triggerButtonClass() { triggerButtonClass() {
......
...@@ -73,7 +73,7 @@ export default class ProjectFindFile { ...@@ -73,7 +73,7 @@ export default class ProjectFindFile {
// find file // find file
} }
// files pathes load // files pathes load
load(url) { load(url) {
axios.get(url) axios.get(url)
.then(({ data }) => { .then(({ data }) => {
...@@ -85,7 +85,7 @@ export default class ProjectFindFile { ...@@ -85,7 +85,7 @@ export default class ProjectFindFile {
.catch(() => flash(__('An error occurred while loading filenames'))); .catch(() => flash(__('An error occurred while loading filenames')));
} }
// render result // render result
renderList(filePaths, searchText) { renderList(filePaths, searchText) {
var blobItemUrl, filePath, html, i, j, len, matches, results; var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty(); this.element.find(".tree-table > tbody").empty();
......
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
let hasUserDefinedProjectPath = false; let hasUserDefinedProjectPath = false;
const deriveProjectPathFromUrl = ($projectImportUrl) => { const deriveProjectPathFromUrl = ($projectImportUrl) => {
...@@ -36,6 +38,7 @@ const bindEvents = () => { ...@@ -36,6 +38,7 @@ const bindEvents = () => {
const $changeTemplateBtn = $('.change-template'); const $changeTemplateBtn = $('.change-template');
const $selectedIcon = $('.selected-icon svg'); const $selectedIcon = $('.selected-icon svg');
const $templateProjectNameInput = $('#template-project-name #project_path'); const $templateProjectNameInput = $('#template-project-name #project_path');
const $pushNewProjectTipTrigger = $('.push-new-project-tip');
if ($newProjectForm.length !== 1) { if ($newProjectForm.length !== 1) {
return; return;
...@@ -55,6 +58,34 @@ const bindEvents = () => { ...@@ -55,6 +58,34 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`); $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`);
}); });
if ($pushNewProjectTipTrigger) {
$pushNewProjectTipTrigger
.removeAttr('rel')
.removeAttr('target')
.on('click', (e) => { e.preventDefault(); })
.popover({
title: $pushNewProjectTipTrigger.data('title'),
placement: 'auto bottom',
html: 'true',
content: $('.push-new-project-tip-template').html(),
})
.on('shown.bs.popover', () => {
$(document).on('click.popover touchstart.popover', (event) => {
if ($(event.target).closest('.popover').length === 0) {
$pushNewProjectTipTrigger.trigger('click');
}
});
const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus');
addSelectOnFocusBehaviour(target);
target.focus();
})
.on('hide.bs.popover', () => {
$(document).off('click.popover touchstart.popover');
});
}
function chooseTemplate() { function chooseTemplate() {
$('.template-option').hide(); $('.template-option').hide();
$projectFieldsForm.addClass('selected'); $projectFieldsForm.addClass('selected');
......
import renderMath from './render_math'; import renderMath from './render_math';
import renderMermaid from './render_mermaid'; import renderMermaid from './render_mermaid';
import syntaxHighlight from './syntax_highlight'; import syntaxHighlight from './syntax_highlight';
// Render Gitlab flavoured Markdown // Render Gitlab flavoured Markdown
// //
// Delegates to syntax highlight and render math & mermaid diagrams. // Delegates to syntax highlight and render math & mermaid diagrams.
......
<script> <script>
import Flash from '../../../flash'; import Flash from '~/flash';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable'; import issuableMixin from '../../../vue_shared/mixins/issuable';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
...@@ -53,8 +53,7 @@ ...@@ -53,8 +53,7 @@
discussion_locked: locked, discussion_locked: locked,
}) })
.then(() => location.reload()) .then(() => location.reload())
.catch(() => Flash(this.__(`Something went wrong trying to .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`)));
change the locked state of this ${this.issuableDisplayName}`)));
}, },
}, },
}; };
......
...@@ -107,7 +107,8 @@ ...@@ -107,7 +107,8 @@
<template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest">
<div <div
class="accept-merge-holder clearfix class="accept-merge-holder clearfix
js-toggle-container accept-action media space-children"> js-toggle-container accept-action media space-children"
>
<button <button
type="button" type="button"
class="btn btn-sm btn-reopen btn-success" class="btn btn-sm btn-reopen btn-success"
......
...@@ -96,9 +96,7 @@ export default { ...@@ -96,9 +96,7 @@ export default {
cb.call(null, data); cb.call(null, data);
} }
}) })
.catch(() => { .catch(() => new Flash('Something went wrong. Please try again.'));
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
}, },
initPolling() { initPolling() {
this.pollingInterval = new SmartInterval({ this.pollingInterval = new SmartInterval({
...@@ -146,9 +144,7 @@ export default { ...@@ -146,9 +144,7 @@ export default {
Project.initRefSwitcher(); Project.initRefSwitcher();
} }
}) })
.catch(() => { .catch(() => new Flash('Something went wrong. Please try again.'));
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
}, },
handleNotification(data) { handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return; if (data.ci_status === this.mr.ciStatus) return;
......
...@@ -4,7 +4,6 @@ import { stateKey } from './state_maps'; ...@@ -4,7 +4,6 @@ import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility'; import { formatDate } from '../../lib/utils/datetime_utility';
export default class MergeRequestStore { export default class MergeRequestStore {
constructor(data) { constructor(data) {
this.sha = data.diff_head_sha; this.sha = data.diff_head_sha;
this.gitlabLogo = data.gitlabLogo; this.gitlabLogo = data.gitlabLogo;
...@@ -169,5 +168,4 @@ export default class MergeRequestStore { ...@@ -169,5 +168,4 @@ export default class MergeRequestStore {
return timeagoInstance.format(date); return timeagoInstance.format(date);
} }
} }
...@@ -6,12 +6,12 @@ ...@@ -6,12 +6,12 @@
import userAvatarImage from './user_avatar/user_avatar_image.vue'; import userAvatarImage from './user_avatar/user_avatar_image.vue';
/** /**
* Renders header component for job and pipeline page based on UI mockups * Renders header component for job and pipeline page based on UI mockups
* *
* Used in: * Used in:
* - job show page * - job show page
* - pipeline show page * - pipeline show page
*/ */
export default { export default {
components: { components: {
ciIconBadge, ciIconBadge,
...@@ -118,7 +118,8 @@ ...@@ -118,7 +118,8 @@
<section <section
class="header-action-buttons" class="header-action-buttons"
v-if="actions.length"> v-if="actions.length"
>
<template <template
v-for="(action, i) in actions" v-for="(action, i) in actions"
> >
......
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
/* This is a re-usable vue component for rendering a button /* This is a re-usable vue component for rendering a button
that will probably be sending off ajax requests and need that will probably be sending off ajax requests and need
to show the loading status by setting the `loading` option. to show the loading status by setting the `loading` option.
......
...@@ -65,7 +65,8 @@ ...@@ -65,7 +65,8 @@
</li> </li>
<li <li
class="md-header-tab" class="md-header-tab"
:class="{ active: previewMarkdown }"> :class="{ active: previewMarkdown }"
>
<a <a
class="js-preview-link" class="js-preview-link"
href="#md-preview-holder" href="#md-preview-holder"
......
...@@ -13,6 +13,12 @@ ...@@ -13,6 +13,12 @@
props: { props: {
/** /**
This function will take the information given by the pagination component This function will take the information given by the pagination component
Here is an example `change` method:
change(pagenum) {
gl.utils.visitUrl(`?page=${pagenum}`);
},
*/ */
change: { change: {
type: Function, type: Function,
......
...@@ -444,6 +444,19 @@ ...@@ -444,6 +444,19 @@
} }
} }
.btn-missing {
color: $notes-light-color;
border: 1px dashed $border-gray-normal-dashed;
border-radius: $border-radius-default;
&:hover,
&:active,
&:focus {
color: $notes-light-color;
background-color: $white-normal;
}
}
.btn-svg svg { .btn-svg svg {
@include btn-svg; @include btn-svg;
} }
......
...@@ -63,10 +63,6 @@ ...@@ -63,10 +63,6 @@
} }
} }
.project-stats {
display: none;
}
.group-buttons { .group-buttons {
display: none; display: none;
} }
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
.container-fluid { .container-fluid {
background: $white-light;
padding: 0 $gl-padding; padding: 0 $gl-padding;
&.container-blank { &.container-blank {
......
...@@ -296,7 +296,7 @@ body { ...@@ -296,7 +296,7 @@ body {
line-height: 1.3; line-height: 1.3;
font-size: 1.25em; font-size: 1.25em;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
margin: 12px 7px; margin: 12px 0;
} }
h1, h1,
...@@ -333,6 +333,10 @@ a > code { ...@@ -333,6 +333,10 @@ a > code {
font-family: $monospace_font; font-family: $monospace_font;
} }
.weight-normal {
font-weight: $gl-font-weight-normal;
}
.commit-sha, .commit-sha,
.ref-name { .ref-name {
@extend .monospace; @extend .monospace;
......
...@@ -215,8 +215,8 @@ $tooltip-font-size: 12px; ...@@ -215,8 +215,8 @@ $tooltip-font-size: 12px;
*/ */
$gl-padding: 16px; $gl-padding: 16px;
$gl-padding-8: 8px; $gl-padding-8: 8px;
$gl-padding-4: 4px;
$gl-col-padding: 15px; $gl-col-padding: 15px;
$gl-btn-padding: 10px;
$gl-input-padding: 10px; $gl-input-padding: 10px;
$gl-vert-padding: 6px; $gl-vert-padding: 6px;
$gl-padding-top: 10px; $gl-padding-top: 10px;
...@@ -377,6 +377,10 @@ $inactive-badge-background: rgba(0, 0, 0, .08); ...@@ -377,6 +377,10 @@ $inactive-badge-background: rgba(0, 0, 0, .08);
$btn-active-gray: #ececec; $btn-active-gray: #ececec;
$btn-active-gray-light: e4e7ed; $btn-active-gray-light: e4e7ed;
$btn-white-active: #848484; $btn-white-active: #848484;
$gl-btn-padding: 10px;
$gl-btn-line-height: 16px;
$gl-btn-vert-padding: 8px;
$gl-btn-horz-padding: 12px;
/* /*
* Badges * Badges
......
...@@ -678,6 +678,9 @@ a.deploy-project-label { ...@@ -678,6 +678,9 @@ a.deploy-project-label {
} }
} }
.project-empty-note-panel {
border-bottom: 1px solid $border-color;
}
.project-stats { .project-stats {
font-size: 0; font-size: 0;
...@@ -686,11 +689,13 @@ a.deploy-project-label { ...@@ -686,11 +689,13 @@ a.deploy-project-label {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
.nav { .nav {
padding-top: 12px; margin-top: $gl-padding-8;
padding-bottom: 12px; margin-bottom: $gl-padding-8;
> li { > li {
display: inline-block; display: inline-block;
margin-top: $gl-padding-4;
margin-bottom: $gl-padding-4;
&:not(:last-child) { &:not(:last-child) {
margin-right: $gl-padding; margin-right: $gl-padding;
...@@ -704,36 +709,32 @@ a.deploy-project-label { ...@@ -704,36 +709,32 @@ a.deploy-project-label {
float: right; float: right;
} }
} }
}
> a { .stat-text,
padding: 0; .stat-link {
background-color: transparent; padding: $gl-btn-vert-padding 0;
font-size: 14px; background-color: transparent;
line-height: 29px; font-size: $gl-font-size;
color: $notes-light-color; line-height: $gl-btn-line-height;
color: $notes-light-color;
}
&:hover, .stat-link {
&:focus { &:hover,
color: $gl-text-color; &:focus {
text-decoration: underline; color: $gl-text-color;
} text-decoration: underline;
} }
} }
}
li.missing { .btn {
border: 1px dashed $border-gray-normal-dashed; padding: $gl-btn-vert-padding $gl-btn-horz-padding;
border-radius: $border-radius-default; line-height: $gl-btn-line-height;
a {
padding-left: 10px;
padding-right: 10px;
color: $notes-light-color;
display: block;
} }
&:hover { .btn-missing {
background-color: $gray-normal; @extend .btn-missing;
} }
} }
} }
...@@ -743,7 +744,7 @@ pre.light-well { ...@@ -743,7 +744,7 @@ pre.light-well {
} }
.git-empty { .git-empty {
margin: 0 7px 7px; margin-bottom: 7px;
h5 { h5 {
color: $gl-text-color; color: $gl-text-color;
...@@ -895,6 +896,12 @@ pre.light-well { ...@@ -895,6 +896,12 @@ pre.light-well {
} }
} }
.project-tip-command {
> .input-group-btn:first-child {
width: auto;
}
}
.protected-branches-list, .protected-branches-list,
.protected-tags-list { .protected-tags-list {
margin-bottom: 30px; margin-bottom: 30px;
......
...@@ -126,10 +126,15 @@ class ApplicationController < ActionController::Base ...@@ -126,10 +126,15 @@ class ApplicationController < ActionController::Base
Ability.allowed?(object, action, subject) Ability.allowed?(object, action, subject)
end end
def access_denied! def access_denied!(message = nil)
respond_to do |format| respond_to do |format|
format.json { head :not_found } format.any { head :not_found }
format.any { render "errors/access_denied", layout: "errors", status: 404 } format.html do
render "errors/access_denied",
layout: "errors",
status: 404,
locals: { message: message }
end
end end
end end
......
...@@ -55,7 +55,7 @@ module Boards ...@@ -55,7 +55,7 @@ module Boards
end end
def issue def issue
@issue ||= issues_finder.execute.find(params[:id]) @issue ||= issues_finder.find(params[:id])
end end
def filter_params def filter_params
......
module ControllerWithCrossProjectAccessCheck
extend ActiveSupport::Concern
included do
extend Gitlab::CrossProjectAccess::ClassMethods
before_action :cross_project_check
end
def cross_project_check
if Gitlab::CrossProjectAccess.find_check(self)&.should_run?(self)
authorize_cross_project_page!
end
end
def authorize_cross_project_page!
return if can?(current_user, :read_cross_project)
rejection_message = _(
"This page is unavailable because you are not allowed to read information "\
"across multiple projects."
)
access_denied!(rejection_message)
end
end
...@@ -3,16 +3,20 @@ module RoutableActions ...@@ -3,16 +3,20 @@ module RoutableActions
def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil) def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil)
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?) routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
if routable_authorized?(routable, extra_authorization_proc) if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path) ensure_canonical_path(routable, requested_full_path)
routable routable
else else
route_not_found handle_not_found_or_authorized(routable)
nil nil
end end
end end
# This is overridden in gitlab-ee.
def handle_not_found_or_authorized(_routable)
route_not_found
end
def routable_authorized?(routable, extra_authorization_proc) def routable_authorized?(routable, extra_authorization_proc)
action = :"read_#{routable.class.to_s.underscore}" action = :"read_#{routable.class.to_s.underscore}"
return false unless can?(current_user, action, routable) return false unless can?(current_user, action, routable)
......
...@@ -24,7 +24,7 @@ module UploadsActions ...@@ -24,7 +24,7 @@ module UploadsActions
# - or redirect to its URL # - or redirect to its URL
# #
def show def show
return render_404 unless uploader.exists? return render_404 unless uploader&.exists?
if uploader.file_storage? if uploader.file_storage?
disposition = uploader.image_or_video? ? 'inline' : 'attachment' disposition = uploader.image_or_video? ? 'inline' : 'attachment'
...@@ -71,6 +71,9 @@ module UploadsActions ...@@ -71,6 +71,9 @@ module UploadsActions
def build_uploader_from_params def build_uploader_from_params
uploader = uploader_class.new(model, secret: params[:secret]) uploader = uploader_class.new(model, secret: params[:secret])
return nil unless uploader.model_valid?
uploader.retrieve_from_store!(params[:filename]) uploader.retrieve_from_store!(params[:filename])
uploader uploader
end end
......
class Dashboard::ApplicationController < ApplicationController class Dashboard::ApplicationController < ApplicationController
include ControllerWithCrossProjectAccessCheck
layout 'dashboard' layout 'dashboard'
requires_cross_project_access
private private
def projects def projects
......
class Dashboard::GroupsController < Dashboard::ApplicationController class Dashboard::GroupsController < Dashboard::ApplicationController
include GroupTree include GroupTree
skip_cross_project_access_check :index
def index def index
groups = GroupsFinder.new(current_user, all_available: false).execute groups = GroupsFinder.new(current_user, all_available: false).execute
render_group_tree(groups) render_group_tree(groups)
......
...@@ -4,6 +4,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -4,6 +4,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
before_action :set_non_archived_param before_action :set_non_archived_param
before_action :default_sorting before_action :default_sorting
skip_cross_project_access_check :index, :starred
def index def index
@projects = load_projects(params.merge(non_public: true)).page(params[:page]) @projects = load_projects(params.merge(non_public: true)).page(params[:page])
......
class Dashboard::SnippetsController < Dashboard::ApplicationController class Dashboard::SnippetsController < Dashboard::ApplicationController
skip_cross_project_access_check :index
def index def index
@snippets = SnippetsFinder.new( @snippets = SnippetsFinder.new(
current_user, current_user,
......
class Groups::ApplicationController < ApplicationController class Groups::ApplicationController < ApplicationController
include RoutableActions include RoutableActions
include ControllerWithCrossProjectAccessCheck
layout 'group' layout 'group'
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :group before_action :group
requires_cross_project_access
private private
......
class Groups::AvatarsController < Groups::ApplicationController class Groups::AvatarsController < Groups::ApplicationController
before_action :authorize_admin_group! before_action :authorize_admin_group!
skip_cross_project_access_check :destroy
def destroy def destroy
@group.remove_avatar! @group.remove_avatar!
@group.save @group.save
......
module Groups module Groups
class ChildrenController < Groups::ApplicationController class ChildrenController < Groups::ApplicationController
before_action :group before_action :group
skip_cross_project_access_check :index
def index def index
parent = if params[:parent_id].present? parent = if params[:parent_id].present?
......
...@@ -6,6 +6,10 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -6,6 +6,10 @@ class Groups::GroupMembersController < Groups::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access] before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite,
:override
def index def index
@sort = params[:sort].presence || sort_value_name @sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id] @project = @group.projects.find(params[:project_id]) if params[:project_id]
......
module Groups module Groups
module Settings module Settings
class CiCdController < Groups::ApplicationController class CiCdController < Groups::ApplicationController
skip_cross_project_access_check :show
before_action :authorize_admin_pipeline! before_action :authorize_admin_pipeline!
def show def show
......
...@@ -2,6 +2,8 @@ module Groups ...@@ -2,6 +2,8 @@ module Groups
class VariablesController < Groups::ApplicationController class VariablesController < Groups::ApplicationController
before_action :authorize_admin_build! before_action :authorize_admin_build!
skip_cross_project_access_check :show, :update
def show def show
respond_to do |format| respond_to do |format|
format.json do format.json do
......
...@@ -19,6 +19,12 @@ class GroupsController < Groups::ApplicationController ...@@ -19,6 +19,12 @@ class GroupsController < Groups::ApplicationController
before_action :user_actions, only: [:show, :subgroups] before_action :user_actions, only: [:show, :subgroups]
skip_cross_project_access_check :index, :new, :create, :edit, :update,
:destroy, :projects
# When loading show as an atom feed, we render events that could leak cross
# project information
skip_cross_project_access_check :show, if: -> { request.format.html? }
layout :determine_layout layout :determine_layout
def index def index
......
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::GonHelper include Gitlab::GonHelper
include Gitlab::Allowable
include PageLayoutHelper include PageLayoutHelper
include OauthApplications include OauthApplications
...@@ -8,6 +9,8 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -8,6 +9,8 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
before_action :add_gon_variables before_action :add_gon_variables
before_action :load_scopes, only: [:index, :create, :edit] before_action :load_scopes, only: [:index, :create, :edit]
helper_method :can?
layout 'profile' layout 'profile'
def index def index
......
...@@ -34,9 +34,9 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController ...@@ -34,9 +34,9 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
def target def target
case params[:type]&.downcase case params[:type]&.downcase
when 'issue' when 'issue'
IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id])
when 'mergerequest' when 'mergerequest'
MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id])
when 'commit' when 'commit'
@project.commit(params[:type_id]) @project.commit(params[:type_id])
end end
......
...@@ -133,7 +133,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -133,7 +133,7 @@ class Projects::BlobController < Projects::ApplicationController
end end
def after_edit_path def after_edit_path
from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:from_merge_request_iid]) from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:from_merge_request_iid])
if from_merge_request && @branch_name == @ref if from_merge_request && @branch_name == @ref
diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) + diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) +
"##{hexdigest(@path)}" "##{hexdigest(@path)}"
......
...@@ -75,7 +75,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -75,7 +75,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def branch_to def branch_to
@target_project = selected_target_project @target_project = selected_target_project
if params[:ref].present? if @target_project && params[:ref].present?
@ref = params[:ref] @ref = params[:ref]
@commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref) @commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end end
...@@ -85,7 +85,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -85,7 +85,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def update_branches def update_branches
@target_project = selected_target_project @target_project = selected_target_project
@target_branches = @target_project.repository.branch_names @target_branches = @target_project ? @target_project.repository.branch_names : []
render layout: false render layout: false
end end
...@@ -121,7 +121,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -121,7 +121,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@project @project
elsif params[:target_project_id].present? elsif params[:target_project_id].present?
MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project) MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project)
.execute.find(params[:target_project_id]) .find_by(id: params[:target_project_id])
else else
@project.forked_from_project @project.forked_from_project
end end
......
...@@ -45,7 +45,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -45,7 +45,7 @@ class ProjectsController < Projects::ApplicationController
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name } notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
) )
else else
render 'new' render 'new', locals: { active_tab: ('import' if project_params[:import_url].present?) }
end end
end end
...@@ -114,6 +114,8 @@ class ProjectsController < Projects::ApplicationController ...@@ -114,6 +114,8 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
@notification_setting = current_user.notification_settings_for(@project) if current_user @notification_setting = current_user.notification_settings_for(@project) if current_user
@project = @project.present(current_user: current_user)
render_landing_page render_landing_page
end end
......
class SearchController < ApplicationController class SearchController < ApplicationController
skip_before_action :authenticate_user! include ControllerWithCrossProjectAccessCheck
include SearchHelper include SearchHelper
include RendersCommits include RendersCommits
skip_before_action :authenticate_user!
requires_cross_project_access if: -> do
search_term_present = params[:search].present? || params[:term].present?
search_term_present && !params[:project_id].present?
end
layout 'search' layout 'search'
def show def show
......
class UsersController < ApplicationController class UsersController < ApplicationController
include RoutableActions include RoutableActions
include RendersMemberAccess include RendersMemberAccess
include ControllerWithCrossProjectAccessCheck
requires_cross_project_access show: false,
groups: false,
projects: false,
contributed: false,
snippets: true,
calendar: false,
calendar_activities: true
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :user, except: [:exists] before_action :user, except: [:exists]
...@@ -103,12 +112,7 @@ class UsersController < ApplicationController ...@@ -103,12 +112,7 @@ class UsersController < ApplicationController
end end
def load_events def load_events
# Get user activity feed for projects common for both users @events = UserRecentEventsFinder.new(current_user, user, params).execute
@events = user.recent_events
.merge(projects_for_current_user)
.references(:project)
.with_associations
.limit_recent(20, params[:offset])
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?) Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end end
...@@ -141,10 +145,6 @@ class UsersController < ApplicationController ...@@ -141,10 +145,6 @@ class UsersController < ApplicationController
).execute.page(params[:page]) ).execute.page(params[:page])
end end
def projects_for_current_user
ProjectsFinder.new(current_user: current_user).execute
end
def build_canonical_path(user) def build_canonical_path(user)
url_for(params.merge(username: user.to_param)) url_for(params.merge(username: user.to_param))
end end
......
module FinderMethods
def find_by!(*args)
raise_not_found_unless_authorized execute.find_by!(*args)
end
def find_by(*args)
if_authorized execute.find_by(*args)
end
def find(*args)
raise_not_found_unless_authorized model.find(*args)
end
private
def raise_not_found_unless_authorized(result)
result = if_authorized(result)
raise ActiveRecord::RecordNotFound.new("Couldn't find #{model}") unless result
result
end
def if_authorized(result)
# Return the result if the finder does not perform authorization checks.
# this is currently the case in the `MilestoneFinder`
return result unless respond_to?(:current_user)
if can_read_object?(result)
result
else
nil
end
end
def can_read_object?(object)
# When there's no policy, we'll allow the read, this is for example the case
# for Todos
return true unless DeclarativePolicy.has_policy?(object)
model_name = object&.model_name || model.model_name
Ability.allowed?(current_user, :"read_#{model_name.singular}", object)
end
# This fetches the model from the `ActiveRecord::Relation` but does not
# actually execute the query.
def model
execute.model
end
end
# Module to prepend into finders to specify wether or not the finder requires
# cross project access
#
# This module depends on the finder implementing the following methods:
#
# - `#execute` should return an `ActiveRecord::Relation`
# - `#current_user` the user that requires access (or nil)
module FinderWithCrossProjectAccess
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
extend Gitlab::CrossProjectAccess::ClassMethods
end
override :execute
def execute(*args)
check = Gitlab::CrossProjectAccess.find_check(self)
original = super
return original unless check
return original if should_skip_cross_project_check || can_read_cross_project?
if check.should_run?(self)
original.model.none
else
original
end
end
# We can skip the cross project check for finding indivitual records.
# this would be handled by the `can?(:read_*, result)` call in `FinderMethods`
# itself.
override :find_by!
def find_by!(*args)
skip_cross_project_check { super }
end
override :find_by
def find_by(*args)
skip_cross_project_check { super }
end
override :find
def find(*args)
skip_cross_project_check { super }
end
private
attr_accessor :should_skip_cross_project_check
def skip_cross_project_check
self.should_skip_cross_project_check = true
yield
ensure
# The find could raise an `ActiveRecord::RecordNotFound`, after which we
# still want to re-enable the check.
self.should_skip_cross_project_check = false
end
def can_read_cross_project?
Ability.allowed?(current_user, :read_cross_project)
end
def can_read_project?(project)
Ability.allowed?(current_user, :read_project, project)
end
end
class EventsFinder class EventsFinder
prepend FinderMethods
prepend FinderWithCrossProjectAccess
attr_reader :source, :params, :current_user attr_reader :source, :params, :current_user
requires_cross_project_access unless: -> { source.is_a?(Project) }
# Used to filter Events # Used to filter Events
# #
# Arguments: # Arguments:
......
...@@ -21,8 +21,12 @@ ...@@ -21,8 +21,12 @@
# my_reaction_emoji: string # my_reaction_emoji: string
# #
class IssuableFinder class IssuableFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include CreatedAtFilter include CreatedAtFilter
requires_cross_project_access unless: -> { project? }
NONE = '0'.freeze NONE = '0'.freeze
attr_accessor :current_user, :params attr_accessor :current_user, :params
...@@ -87,14 +91,6 @@ class IssuableFinder ...@@ -87,14 +91,6 @@ class IssuableFinder
by_my_reaction_emoji(items) by_my_reaction_emoji(items)
end end
def find(*params)
execute.find(*params)
end
def find_by(*params)
execute.find_by(*params)
end
def row_count def row_count
Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state]) Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state])
end end
...@@ -124,10 +120,6 @@ class IssuableFinder ...@@ -124,10 +120,6 @@ class IssuableFinder
counts counts
end end
def find_by!(*params)
execute.find_by!(*params)
end
def group def group
return @group if defined?(@group) return @group if defined?(@group)
......
class LabelsFinder < UnionFinder class LabelsFinder < UnionFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
requires_cross_project_access unless: -> { project? }
def initialize(current_user, params = {}) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
@params = params @params = params
......
class MergeRequestTargetProjectFinder class MergeRequestTargetProjectFinder
include FinderMethods
attr_reader :current_user, :source_project attr_reader :current_user, :source_project
def initialize(current_user: nil, source_project:) def initialize(current_user: nil, source_project:)
......
...@@ -8,6 +8,8 @@ ...@@ -8,6 +8,8 @@
# state - filters by state. # state - filters by state.
class MilestonesFinder class MilestonesFinder
include FinderMethods
attr_reader :params, :project_ids, :group_ids attr_reader :params, :project_ids, :group_ids
def initialize(params = {}) def initialize(params = {})
......
...@@ -13,7 +13,9 @@ ...@@ -13,7 +13,9 @@
# params are optional # params are optional
class SnippetsFinder < UnionFinder class SnippetsFinder < UnionFinder
include Gitlab::Allowable include Gitlab::Allowable
attr_accessor :current_user, :params, :project include FinderMethods
attr_accessor :current_user, :project, :params
def initialize(current_user, params = {}) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
...@@ -52,10 +54,14 @@ class SnippetsFinder < UnionFinder ...@@ -52,10 +54,14 @@ class SnippetsFinder < UnionFinder
end end
def authorized_snippets def authorized_snippets
Snippet.where(feature_available_projects.or(not_project_related)).public_or_visible_to_user(current_user) Snippet.where(feature_available_projects.or(not_project_related))
.public_or_visible_to_user(current_user)
end end
def feature_available_projects def feature_available_projects
# Don't return any project related snippets if the user cannot read cross project
return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project)
projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part| projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part|
part.with_feature_available_for_user(:snippets, current_user) part.with_feature_available_for_user(:snippets, current_user)
end.select(:id) end.select(:id)
......
...@@ -13,6 +13,11 @@ ...@@ -13,6 +13,11 @@
# #
class TodosFinder class TodosFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
requires_cross_project_access unless: -> { project? }
NONE = '0'.freeze NONE = '0'.freeze
attr_accessor :current_user, :params attr_accessor :current_user, :params
......
# Get user activity feed for projects common for a user and a logged in user
#
# - current_user: The user viewing the events
# - user: The user for which to load the events
# - params:
# - offset: The page of events to return
class UserRecentEventsFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
requires_cross_project_access
attr_reader :current_user, :target_user, :params
def initialize(current_user, target_user, params = {})
@current_user = current_user
@target_user = target_user
@params = params
end
def execute
target_user
.recent_events
.merge(projects_for_current_user)
.references(:project)
.with_associations
.limit_recent(20, params[:offset])
end
def projects_for_current_user
ProjectsFinder.new(current_user: current_user).execute
end
end
...@@ -34,7 +34,7 @@ module ApplicationHelper ...@@ -34,7 +34,7 @@ module ApplicationHelper
def project_icon(project_id, options = {}) def project_icon(project_id, options = {})
project = project =
if project_id.is_a?(Project) if project_id.respond_to?(:avatar_url)
project_id project_id
else else
Project.find_by_full_path(project_id) Project.find_by_full_path(project_id)
......
...@@ -10,12 +10,6 @@ module BranchesHelper ...@@ -10,12 +10,6 @@ module BranchesHelper
project_branches_path(@project, @id, options) project_branches_path(@project, @id, options)
end end
def can_push_branch?(project, branch_name)
return false unless project.repository.branch_exists?(branch_name)
::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch_name)
end
def project_branches def project_branches
options_for_select(@project.repository.branch_names, @project.default_branch) options_for_select(@project.repository.branch_names, @project.default_branch)
end end
......
...@@ -6,4 +6,28 @@ module DashboardHelper ...@@ -6,4 +6,28 @@ module DashboardHelper
def assigned_mrs_dashboard_path def assigned_mrs_dashboard_path
merge_requests_dashboard_path(assignee_id: current_user.id) merge_requests_dashboard_path(assignee_id: current_user.id)
end end
def dashboard_nav_links
@dashboard_nav_links ||= get_dashboard_nav_links
end
def dashboard_nav_link?(link)
dashboard_nav_links.include?(link)
end
def any_dashboard_nav_link?(links)
links.any? { |link| dashboard_nav_link?(link) }
end
private
def get_dashboard_nav_links
links = [:projects, :groups, :snippets]
if can?(current_user, :read_cross_project)
links += [:activity, :milestones]
end
links
end
end end
...@@ -25,8 +25,24 @@ module ExploreHelper ...@@ -25,8 +25,24 @@ module ExploreHelper
controller.class.name.split("::").first == "Explore" controller.class.name.split("::").first == "Explore"
end end
def explore_nav_links
@explore_nav_links ||= get_explore_nav_links
end
def explore_nav_link?(link)
explore_nav_links.include?(link)
end
def any_explore_nav_link?(links)
links.any? { |link| explore_nav_link?(link) }
end
private private
def get_explore_nav_links
[:projects, :groups, :snippets]
end
def request_path_with_options(options = {}) def request_path_with_options(options = {})
request.path + "?#{options.to_param}" request.path + "?#{options.to_param}"
end end
......
...@@ -3,6 +3,14 @@ module GroupsHelper ...@@ -3,6 +3,14 @@ module GroupsHelper
%w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index] %w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
end end
def group_sidebar_links
@group_sidebar_links ||= get_group_sidebar_links
end
def group_sidebar_link?(link)
group_sidebar_links.include?(link)
end
def can_change_group_visibility_level?(group) def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group) can?(current_user, :change_visibility_level, group)
end end
...@@ -107,6 +115,20 @@ module GroupsHelper ...@@ -107,6 +115,20 @@ module GroupsHelper
private private
def get_group_sidebar_links
links = [:overview, :group_members]
if can?(current_user, :read_cross_project)
links += [:activity, :issues, :labels, :milestones, :merge_requests]
end
if can?(current_user, :admin_group, @group)
links << :settings
end
links
end
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false) def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do
output = output =
......
...@@ -47,27 +47,6 @@ module IssuesHelper ...@@ -47,27 +47,6 @@ module IssuesHelper
end end
end end
def milestone_options(object)
milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed?
milestones.unshift(Milestone::None)
options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
end
def project_options(issuable, current_user, ability: :read_project)
projects = current_user.authorized_projects.order_id_desc
projects = projects.select do |project|
current_user.can?(ability, project)
end
no_project = OpenStruct.new(id: 0, name_with_namespace: 'No project')
projects.unshift(no_project)
projects.delete(issuable.project)
options_from_collection_for_select(projects, :id, :name_with_namespace)
end
def status_box_class(item) def status_box_class(item)
if item.try(:expired?) if item.try(:expired?)
'status-box-expired' 'status-box-expired'
......
module NavHelper module NavHelper
def header_links
@header_links ||= get_header_links
end
def header_link?(link)
header_links.include?(link)
end
def page_with_sidebar_class def page_with_sidebar_class
class_name = page_gutter_class class_name = page_gutter_class
class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
...@@ -38,4 +46,28 @@ module NavHelper ...@@ -38,4 +46,28 @@ module NavHelper
class_names class_names
end end
private
def get_header_links
links = if current_user
[:user_dropdown]
else
[:sign_in]
end
if can?(current_user, :read_cross_project)
links += [:issues, :merge_requests, :todos] if current_user.present?
end
if @project&.persisted? || can?(current_user, :read_cross_project)
links << :search
end
if session[:impersonator_id]
links << :admin_impersonation
end
links
end
end end
...@@ -48,30 +48,4 @@ module PreferencesHelper ...@@ -48,30 +48,4 @@ module PreferencesHelper
def user_color_scheme def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class Gitlab::ColorSchemes.for_user(current_user).css_class
end end
def default_project_view
return anonymous_project_view unless current_user
user_view = current_user.project_view
if can?(current_user, :download_code, @project)
user_view
elsif user_view == "activity"
"activity"
elsif can?(current_user, :read_wiki, @project)
"wiki"
elsif @project.feature_available?(:issues, current_user)
"projects/issues/issues"
else
"customize_workflow"
end
end
def anonymous_project_view
if !@project.empty_repo? && can?(current_user, :download_code, @project)
'files'
else
'activity'
end
end
end end
...@@ -153,11 +153,6 @@ module ProjectsHelper ...@@ -153,11 +153,6 @@ module ProjectsHelper
end end
end end
def license_short_name(project)
license = project.repository.license
license&.nickname || license&.name || 'LICENSE'
end
def last_push_event def last_push_event
current_user&.recent_push(@project) current_user&.recent_push(@project)
end end
...@@ -213,6 +208,7 @@ module ProjectsHelper ...@@ -213,6 +208,7 @@ module ProjectsHelper
controller.controller_name, controller.controller_name,
controller.action_name, controller.action_name,
Gitlab::CurrentSettings.cache_key, Gitlab::CurrentSettings.cache_key,
"cross-project:#{can?(current_user, :read_cross_project)}",
'v2.5' 'v2.5'
] ]
...@@ -265,6 +261,17 @@ module ProjectsHelper ...@@ -265,6 +261,17 @@ module ProjectsHelper
!!(params[:personal] || params[:name] || any_projects?(projects)) !!(params[:personal] || params[:name] || any_projects?(projects))
end end
def push_to_create_project_command(user = current_user)
repository_url =
if Gitlab::CurrentSettings.current_application_settings.enabled_git_access_protocol == 'http'
user_url(user)
else
Gitlab.config.gitlab_shell.ssh_path_prefix + user.username
end
"git push --set-upstream #{repository_url}/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)"
end
private private
def repo_children_classes(field) def repo_children_classes(field)
...@@ -390,55 +397,6 @@ module ProjectsHelper ...@@ -390,55 +397,6 @@ module ProjectsHelper
end end
end end
def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil)
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
project_new_blob_path(
project,
project.default_branch || 'master',
file_name: file_name,
commit_message: commit_message,
branch_name: branch_name,
context: context
)
end
def add_koding_stack_path(project)
project_new_blob_path(
project,
project.default_branch || 'master',
file_name: '.koding.yml',
commit_message: "Add Koding stack script",
content: <<-CONTENT.strip_heredoc
provider:
aws:
access_key: '${var.aws_access_key}'
secret_key: '${var.aws_secret_key}'
resource:
aws_instance:
#{project.path}-vm:
instance_type: t2.nano
user_data: |-
# Created by GitLab UI for :>
echo _KD_NOTIFY_@Installing Base packages...@
apt-get update -y
apt-get install git -y
echo _KD_NOTIFY_@Cloning #{project.name}...@
export KODING_USER=${var.koding_user_username}
export REPO_URL=#{root_url}${var.koding_queryString_repo}.git
export BRANCH=${var.koding_queryString_branch}
sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH
echo _KD_NOTIFY_@#{project.name} cloned.@
CONTENT
)
end
def koding_project_url(project = nil, branch = nil, sha = nil) def koding_project_url(project = nil, branch = nil, sha = nil)
if project if project
import_path = "/Home/Stacks/import" import_path = "/Home/Stacks/import"
...@@ -455,36 +413,6 @@ module ProjectsHelper ...@@ -455,36 +413,6 @@ module ProjectsHelper
Gitlab::CurrentSettings.koding_url Gitlab::CurrentSettings.koding_url
end end
def contribution_guide_path(project)
if project && contribution_guide = project.repository.contribution_guide
project_blob_path(
project,
tree_join(project.default_branch,
contribution_guide.name)
)
end
end
def readme_path(project)
filename_path(project, :readme)
end
def changelog_path(project)
filename_path(project, :changelog)
end
def license_path(project)
filename_path(project, :license_blob)
end
def version_path(project)
filename_path(project, :version)
end
def ci_configuration_path(project)
filename_path(project, :gitlab_ci_yml)
end
def project_wiki_path_with_version(proj, page, version, is_newest) def project_wiki_path_with_version(proj, page, version, is_newest)
url_params = is_newest ? {} : { version_id: version } url_params = is_newest ? {} : { version_id: version }
project_wiki_path(proj, page, url_params) project_wiki_path(proj, page, url_params)
...@@ -510,15 +438,6 @@ module ProjectsHelper ...@@ -510,15 +438,6 @@ module ProjectsHelper
@ref || @repository.try(:root_ref) @ref || @repository.try(:root_ref)
end end
def filename_path(project, filename)
if project && blob = project.repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend
project_blob_path(
project,
tree_join(project.default_branch, blob.name)
)
end
end
def sanitize_repo_path(project, message) def sanitize_repo_path(project, message)
return '' unless message.present? return '' unless message.present?
...@@ -608,4 +527,8 @@ module ProjectsHelper ...@@ -608,4 +527,8 @@ module ProjectsHelper
project_find_file_path(@project, ref) project_find_file_path(@project, ref)
end end
def can_show_last_commit_in_list?(project)
can?(current_user, :read_cross_project) && project.commit
end
end end
...@@ -55,7 +55,9 @@ module TreeHelper ...@@ -55,7 +55,9 @@ module TreeHelper
def tree_edit_branch(project = @project, ref = @ref) def tree_edit_branch(project = @project, ref = @ref)
return unless can_edit_tree?(project, ref) return unless can_edit_tree?(project, ref)
if can_push_branch?(project, ref) project = project.present(current_user: current_user)
if project.can_current_user_push_to_branch?(ref)
ref ref
else else
project = tree_edit_project(project) project = tree_edit_project(project)
......
...@@ -14,4 +14,18 @@ module UsersHelper ...@@ -14,4 +14,18 @@ module UsersHelper
content_tag(:strong) { user.unconfirmed_email } + h('.') + content_tag(:strong) { user.unconfirmed_email } + h('.') +
content_tag(:p) { confirmation_link } content_tag(:p) { confirmation_link }
end end
def profile_tabs
@profile_tabs ||= get_profile_tabs
end
def profile_tab?(tab)
profile_tabs.include?(tab)
end
private
def get_profile_tabs
[:activity, :groups, :contributed, :projects, :snippets]
end
end end
...@@ -22,12 +22,30 @@ class Ability ...@@ -22,12 +22,30 @@ class Ability
# #
# issues - The issues to reduce down to those readable by the user. # issues - The issues to reduce down to those readable by the user.
# user - The User for which to check the issues # user - The User for which to check the issues
def issues_readable_by_user(issues, user = nil) # filters - A hash of abilities and filters to apply if the user lacks this
# ability
def issues_readable_by_user(issues, user = nil, filters: {})
issues = apply_filters_if_needed(issues, user, filters)
DeclarativePolicy.user_scope do DeclarativePolicy.user_scope do
issues.select { |issue| issue.visible_to_user?(user) } issues.select { |issue| issue.visible_to_user?(user) }
end end
end end
# Returns an Array of MergeRequests that can be read by the given user.
#
# merge_requests - MRs out of which to collect mr's readable by the user.
# user - The User for which to check the merge_requests
# filters - A hash of abilities and filters to apply if the user lacks this
# ability
def merge_requests_readable_by_user(merge_requests, user = nil, filters: {})
merge_requests = apply_filters_if_needed(merge_requests, user, filters)
DeclarativePolicy.user_scope do
merge_requests.select { |mr| allowed?(user, :read_merge_request, mr) }
end
end
def can_edit_note?(user, note) def can_edit_note?(user, note)
allowed?(user, :edit_note, note) allowed?(user, :edit_note, note)
end end
...@@ -53,5 +71,15 @@ class Ability ...@@ -53,5 +71,15 @@ class Ability
cache = RequestStore.active? ? RequestStore : {} cache = RequestStore.active? ? RequestStore : {}
DeclarativePolicy.policy_for(user, subject, cache: cache) DeclarativePolicy.policy_for(user, subject, cache: cache)
end end
private
def apply_filters_if_needed(elements, user, filters)
filters.each do |ability, filter|
elements = filter.call(elements) unless allowed?(user, ability)
end
elements
end
end end
end end
...@@ -467,7 +467,7 @@ module Ci ...@@ -467,7 +467,7 @@ module Ci
if cache && project.jobs_cache_index if cache && project.jobs_cache_index
cache = cache.merge( cache = cache.merge(
key: "#{cache[:key]}_#{project.jobs_cache_index}") key: "#{cache[:key]}-#{project.jobs_cache_index}")
end end
[cache] [cache]
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment