summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/blob/viewer/index.js5
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js3
-rw-r--r--app/assets/javascripts/dispatcher.js2
-rw-r--r--app/assets/javascripts/milestone_select.js37
-rw-r--r--app/assets/stylesheets/framework/blocks.scss21
-rw-r--r--app/assets/stylesheets/pages/issues.scss10
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/controllers/dashboard/snippets_controller.rb7
-rw-r--r--app/controllers/explore/groups_controller.rb2
-rw-r--r--app/controllers/explore/snippets_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb5
-rw-r--r--app/controllers/snippets_controller.rb26
-rw-r--r--app/controllers/users_controller.rb7
-rw-r--r--app/finders/groups_finder.rb20
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/finders/snippets_finder.rb102
-rw-r--r--app/helpers/markup_helper.rb12
-rw-r--r--app/helpers/submodule_helper.rb46
-rw-r--r--app/models/readme_blob.rb13
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/snippet.rb13
-rw-r--r--app/models/tree.rb5
-rw-r--r--app/policies/project_snippet_policy.rb2
-rw-r--r--app/services/search/snippet_service.rb2
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/import/base/create.js.haml2
-rw-r--r--app/views/projects/_readme.html.haml6
-rw-r--r--app/views/projects/blob/_viewer.html.haml4
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml3
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml2
-rw-r--r--app/views/projects/diffs/_file_header.html.haml6
-rw-r--r--app/views/projects/imports/new.html.haml2
-rw-r--r--app/views/projects/tree/_readme.html.haml6
-rw-r--r--app/views/projects/wikis/git_access.html.haml2
-rw-r--r--app/views/shared/empty_states/_issues.html.haml4
-rw-r--r--app/views/shared/empty_states/_labels.html.haml4
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml4
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/workers/namespaceless_project_destroy_worker.rb43
-rw-r--r--changelogs/unreleased/30949-empty-states.yml4
-rw-r--r--changelogs/unreleased/31157-respect-project-features-in-wiki-search.yml4
-rw-r--r--changelogs/unreleased/31474-issue-boards-sidebar-milestone-dropdown-should-not-be-multi-select.yml4
-rw-r--r--changelogs/unreleased/branch-name-escape.yml4
-rw-r--r--changelogs/unreleased/bvl-markup-pipeline.yml4
-rw-r--r--changelogs/unreleased/bvl-validate-urls-in-markdown-using-uri.yml4
-rw-r--r--changelogs/unreleased/dm-async-tree-readme.yml4
-rw-r--r--changelogs/unreleased/hamlit-xss-fix.yml4
-rw-r--r--changelogs/unreleased/rs-sanitize-submodule-urls.yml4
-rw-r--r--changelogs/unreleased/snippets-finder-visibility.yml4
-rw-r--r--changelogs/unreleased/snippets_visibility.yml4
-rw-r--r--changelogs/unreleased/tc-clean-pending-delete-projects.yml4
-rw-r--r--changelogs/unreleased/tc-fix-private-subgroups-shown.yml4
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb47
-rw-r--r--doc/api/access_requests.md2
-rw-r--r--doc/api/enviroments.md2
-rw-r--r--doc/api/groups.md2
-rw-r--r--doc/api/issues.md2
-rw-r--r--doc/api/keys.md2
-rw-r--r--doc/api/labels.md2
-rw-r--r--doc/api/members.md2
-rw-r--r--doc/api/merge_requests.md2
-rw-r--r--doc/api/notification_settings.md2
-rw-r--r--doc/api/projects.md2
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/api/snippets.md2
-rw-r--r--doc/api/todos.md2
-rw-r--r--doc/api/users.md2
-rw-r--r--features/project/project.feature1
-rw-r--r--features/project/source/markdown_render.feature3
-rw-r--r--features/steps/project/project.rb2
-rw-r--r--features/steps/project/source/markdown_render.rb1
-rw-r--r--lib/api/groups.rb2
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/project_snippets.rb3
-rw-r--r--lib/api/snippets.rb4
-rw-r--r--lib/api/v3/groups.rb2
-rw-r--r--lib/api/v3/project_snippets.rb3
-rw-r--r--lib/api/v3/snippets.rb4
-rw-r--r--lib/banzai/filter/external_link_filter.rb36
-rw-r--r--lib/banzai/pipeline/markup_pipeline.rb13
-rw-r--r--lib/gitlab/asciidoc.rb8
-rw-r--r--lib/gitlab/other_markup.rb6
-rw-r--r--lib/gitlab/project_search_results.rb4
-rw-r--r--spec/controllers/groups_controller_spec.rb35
-rw-r--r--spec/controllers/snippets_controller_spec.rb34
-rw-r--r--spec/features/dashboard/milestone_filter_spec.rb59
-rw-r--r--spec/features/dashboard/snippets_spec.rb47
-rw-r--r--spec/features/projects/snippets_spec.rb24
-rw-r--r--spec/features/snippets/explore_spec.rb25
-rw-r--r--spec/features/snippets/internal_snippet_spec.rb23
-rw-r--r--spec/features/users/snippets_spec.rb46
-rw-r--r--spec/finders/groups_finder_spec.rb57
-rw-r--r--spec/finders/snippets_finder_spec.rb125
-rw-r--r--spec/helpers/submodule_helper_spec.rb12
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb85
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb29
-rw-r--r--spec/lib/gitlab/other_markup_spec.rb2
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb75
-rw-r--r--spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb32
-rw-r--r--spec/models/repository_spec.rb2
-rw-r--r--spec/models/snippet_spec.rb40
-rw-r--r--spec/policies/project_snippet_policy_spec.rb80
-rw-r--r--spec/views/projects/imports/new.html.haml_spec.rb22
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb79
108 files changed, 1219 insertions, 386 deletions
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 07d67d49aa5..69ff2f95799 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -8,7 +8,10 @@ export default class BlobViewer {
this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
this.$fileHolder = $('.file-holder');
- let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type');
+ const initialViewer = document.querySelector('.blob-viewer:not(.hidden)');
+ if (!initialViewer) return;
+
+ let initialViewerName = initialViewer.getAttribute('data-type');
this.initBindings();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 317cef9f227..9bcea302da2 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -36,6 +36,9 @@ gl.issueBoards.BoardSidebar = Vue.extend({
},
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
+ },
+ milestoneTitle() {
+ return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
}
},
watch: {
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index abb871c3af0..43ad127a4db 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -246,6 +246,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
new NotificationsForm();
if ($('#tree-slider').length) {
new TreeView();
+ new BlobViewer();
}
break;
case 'projects:pipelines:builds':
@@ -300,6 +301,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
new TreeView();
+ new BlobViewer();
gl.TargetBranchDropDown.bootstrap();
break;
case 'projects:find_file:show':
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 11e68c0a3be..9d481d7c003 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -18,12 +18,11 @@
}
$els.each(function(i, dropdown) {
- var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
+ var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, defaultNo, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, selectedMilestoneDefault, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones');
issueUpdateURL = $dropdown.data('issueUpdate');
- selectedMilestone = $dropdown.data('selected');
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
@@ -31,6 +30,7 @@
showStarted = $dropdown.data('show-started');
useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label');
+ defaultNo = $dropdown.data('default-no');
issuableId = $dropdown.data('issuable-id');
abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox');
@@ -38,6 +38,9 @@
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
+ selectedMilestoneDefault = (showAny ? '' : null);
+ selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
+ selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
@@ -86,8 +89,18 @@
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
+ $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
});
},
+ renderRow: function(milestone) {
+ return `
+ <li data-milestone-id="${milestone.name}">
+ <a href='#' class='dropdown-menu-milestone-link'>
+ ${_.escape(milestone.title)}
+ </a>
+ </li>
+ `;
+ },
filterable: true,
search: {
fields: ['title']
@@ -120,15 +133,24 @@
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
+ opened: function(e) {
+ const $el = $(e.currentTarget);
+ if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
+ }
+ $('a.is-active', $el).removeClass('is-active');
+ $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
+ },
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(options) {
const { $el, e } = options;
let selected = options.selectedObj;
-
- var data, isIssueIndex, isMRIndex, page, boardsStore;
+ var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
+ isSelecting = (selected.name !== selectedMilestone);
+ selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
@@ -142,16 +164,11 @@
boardsStore[$dropdown.data('field-name')] = selected.name;
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- if (selected.name != null) {
- selectedMilestone = selected.name;
- } else {
- selectedMilestone = '';
- }
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
- if (selected.id !== -1) {
+ if (selected.id !== -1 && isSelecting) {
gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
id: selected.id,
title: selected.name
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index ac1fc0eb8ae..3dec911d289 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -312,7 +312,7 @@
}
.empty-state {
- margin: 100px 0 0;
+ margin: 5% auto 0;
.text-content {
max-width: 460px;
@@ -335,27 +335,12 @@
}
.btn {
- margin: $btn-side-margin $btn-side-margin 0 0;
- }
-
- @media(max-width: $screen-xs-max) {
- margin-top: 50px;
- text-align: center;
+ margin: $btn-side-margin 5px;
- .btn {
+ @media(max-width: $screen-xs-max) {
width: 100%;
}
}
-
- @media(min-width: $screen-xs-max) {
- &.merge-requests .text-content {
- margin-top: 40px;
- }
-
- &.labels .text-content {
- margin-top: 70px;
- }
- }
}
.flex-container-block {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index ad3b6e0344b..bee9b13b375 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -51,6 +51,7 @@ ul.related-merge-requests > li {
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
+ align-items: center;
.merge-request-id {
flex-shrink: 0;
@@ -59,6 +60,14 @@ ul.related-merge-requests > li {
.merge-request-info {
margin-left: 5px;
}
+
+ .row_title {
+ vertical-align: bottom;
+ }
+
+ gl-emoji {
+ font-size: 1em;
+ }
}
.merge-requests-title,
@@ -114,7 +123,6 @@ ul.related-merge-requests > li {
.related-merge-requests {
.ci-status-link {
display: block;
- margin-top: 3px;
margin-right: 5px;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index e7553c7a4bf..685b9775fe1 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -8,10 +8,6 @@
white-space: nowrap;
}
- .empty-state {
- margin: 5% auto 0;
- }
-
.table-holder {
width: 100%;
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index bcfdbe14be9..8dd91264451 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -1,11 +1,10 @@
class Dashboard::SnippetsController < Dashboard::ApplicationController
def index
- @snippets = SnippetsFinder.new.execute(
+ @snippets = SnippetsFinder.new(
current_user,
- filter: :by_user,
- user: current_user,
+ author: current_user,
scope: params[:scope]
- )
+ ).execute
@snippets = @snippets.page(params[:page])
end
end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index 68228c095da..81883c543ba 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,6 +1,6 @@
class Explore::GroupsController < Explore::ApplicationController
def index
- @groups = GroupsFinder.new.execute(current_user)
+ @groups = GroupsFinder.new(current_user).execute
@groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page])
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
index 28760c3f84b..d3f0e033068 100644
--- a/app/controllers/explore/snippets_controller.rb
+++ b/app/controllers/explore/snippets_controller.rb
@@ -1,6 +1,6 @@
class Explore::SnippetsController < Explore::ApplicationController
def index
- @snippets = SnippetsFinder.new.execute(current_user, filter: :all)
+ @snippets = SnippetsFinder.new(current_user).execute
@snippets = @snippets.page(params[:page])
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 46c3ff10694..1515173d0ac 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -64,7 +64,7 @@ class GroupsController < Groups::ApplicationController
end
def subgroups
- @nested_groups = group.children
+ @nested_groups = GroupsFinder.new(current_user, parent: group).execute
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 66f913f8f9d..3b2b0d9e502 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -23,12 +23,11 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html
def index
- @snippets = SnippetsFinder.new.execute(
+ @snippets = SnippetsFinder.new(
current_user,
- filter: :by_project,
project: @project,
scope: params[:scope]
- )
+ ).execute
@snippets = @snippets.page(params[:page])
if @snippets.out_of_range? && @snippets.total_pages != 0
redirect_to namespace_project_snippets_path(page: @snippets.total_pages)
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 19e07e3ab86..7445f61195d 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -27,12 +27,8 @@ class SnippetsController < ApplicationController
return render_404 unless @user
- @snippets = SnippetsFinder.new.execute(current_user, {
- filter: :by_user,
- user: @user,
- scope: params[:scope]
- })
- .page(params[:page])
+ @snippets = SnippetsFinder.new(current_user, author: @user, scope: params[:scope])
+ .execute.page(params[:page])
render 'index'
else
@@ -103,20 +99,20 @@ class SnippetsController < ApplicationController
protected
def snippet
- @snippet ||= if current_user
- PersonalSnippet.where("author_id = ? OR visibility_level IN (?)",
- current_user.id,
- [Snippet::PUBLIC, Snippet::INTERNAL]).
- find(params[:id])
- else
- PersonalSnippet.find(params[:id])
- end
+ @snippet ||= PersonalSnippet.find_by(id: params[:id])
end
+
alias_method :awardable, :snippet
alias_method :spammable, :snippet
def authorize_read_snippet!
- authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
+ return if can?(current_user, :read_personal_snippet, @snippet)
+
+ if current_user
+ render_404
+ else
+ authenticate_user!
+ end
end
def authorize_update_snippet!
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index ca89ed221c6..ba22b2f9d29 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -128,12 +128,11 @@ class UsersController < ApplicationController
end
def load_snippets
- @snippets = SnippetsFinder.new.execute(
+ @snippets = SnippetsFinder.new(
current_user,
- filter: :by_user,
- user: user,
+ author: user,
scope: params[:scope]
- ).page(params[:page])
+ ).execute.page(params[:page])
end
def projects_for_current_user
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index d932a17883f..f68610e197c 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -1,13 +1,19 @@
class GroupsFinder < UnionFinder
- def execute(current_user = nil)
- segments = all_groups(current_user)
+ def initialize(current_user = nil, params = {})
+ @current_user = current_user
+ @params = params
+ end
- find_union(segments, Group).with_route.order_id_desc
+ def execute
+ groups = find_union(all_groups, Group).with_route.order_id_desc
+ by_parent(groups)
end
private
- def all_groups(current_user)
+ attr_reader :current_user, :params
+
+ def all_groups
groups = []
groups << current_user.authorized_groups if current_user
@@ -15,4 +21,10 @@ class GroupsFinder < UnionFinder
groups
end
+
+ def by_parent(groups)
+ return groups unless params[:parent]
+
+ groups.where(parent: params[:parent])
+ end
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index dc6a8ad1f66..02eb983bf55 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -67,7 +67,7 @@ class NotesFinder
when "merge_request"
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
when "snippet", "project_snippet"
- SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
+ SnippetsFinder.new(@current_user, project: @project).execute
when "personal_snippet"
PersonalSnippet.all
else
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index da6e6e87a6f..c04f61de79c 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -1,66 +1,74 @@
-class SnippetsFinder
- def execute(current_user, params = {})
- filter = params[:filter]
- user = params.fetch(:user, current_user)
-
- case filter
- when :all then
- snippets(current_user).fresh
- when :public then
- Snippet.are_public.fresh
- when :by_user then
- by_user(current_user, user, params[:scope])
- when :by_project
- by_project(current_user, params[:project], params[:scope])
- end
+class SnippetsFinder < UnionFinder
+ attr_accessor :current_user, :params
+
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ items = init_collection
+ items = by_project(items)
+ items = by_author(items)
+ items = by_visibility(items)
+
+ items.fresh
end
private
- def snippets(current_user)
- if current_user
- Snippet.public_and_internal
- else
- # Not authenticated
- #
- # Return only:
- # public snippets
- Snippet.are_public
- end
+ def init_collection
+ items = Snippet.all
+
+ accessible(items)
end
- def by_user(current_user, user, scope)
- snippets = user.snippets.fresh
+ def accessible(items)
+ segments = []
+ segments << items.public_to_user(current_user)
+ segments << authorized_to_user(items) if current_user
- if current_user
- include_private = user == current_user
- by_scope(snippets, scope, include_private)
- else
- snippets.are_public
- end
+ find_union(segments, Snippet)
end
- def by_project(current_user, project, scope)
- snippets = project.snippets.fresh
+ def authorized_to_user(items)
+ items.where(
+ 'author_id = :author_id
+ OR project_id IN (:project_ids)',
+ author_id: current_user.id,
+ project_ids: current_user.authorized_projects.select(:id))
+ end
- if current_user
- include_private = project.team.member?(current_user) || current_user.admin?
- by_scope(snippets, scope, include_private)
- else
- snippets.are_public
- end
+ def by_visibility(items)
+ visibility = params[:visibility] || visibility_from_scope
+
+ return items unless visibility
+
+ items.where(visibility_level: visibility)
+ end
+
+ def by_author(items)
+ return items unless params[:author]
+
+ items.where(author_id: params[:author].id)
+ end
+
+ def by_project(items)
+ return items unless params[:project]
+
+ items.where(project_id: params[:project].id)
end
- def by_scope(snippets, scope = nil, include_private = false)
- case scope.to_s
+ def visibility_from_scope
+ case params[:scope].to_s
when 'are_private'
- include_private ? snippets.are_private : Snippet.none
+ Snippet::PRIVATE
when 'are_internal'
- snippets.are_internal
+ Snippet::INTERNAL
when 'are_public'
- snippets.are_public
+ Snippet::PUBLIC
else
- include_private ? snippets : snippets.public_and_internal
+ nil
end
end
end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 752d0079818..0009cad86c4 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -116,13 +116,13 @@ module MarkupHelper
if gitlab_markdown?(file_name)
markdown_unsafe(text, context)
elsif asciidoc?(file_name)
- asciidoc_unsafe(text)
+ asciidoc_unsafe(text, context)
elsif plain?(file_name)
content_tag :pre, class: 'plain-readme' do
text
end
else
- other_markup_unsafe(file_name, text)
+ other_markup_unsafe(file_name, text, context)
end
rescue RuntimeError
simple_format(text)
@@ -217,12 +217,12 @@ module MarkupHelper
Banzai.render(text, context)
end
- def asciidoc_unsafe(text)
- Gitlab::Asciidoc.render(text)
+ def asciidoc_unsafe(text, context = {})
+ Gitlab::Asciidoc.render(text, context)
end
- def other_markup_unsafe(file_name, text)
- Gitlab::OtherMarkup.render(file_name, text)
+ def other_markup_unsafe(file_name, text, context = {})
+ Gitlab::OtherMarkup.render(file_name, text, context)
end
def prepare_for_rendering(html, context = {})
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index a762b320d56..b739554a7a4 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -1,28 +1,30 @@
module SubmoduleHelper
include Gitlab::ShellAdapter
+ VALID_SUBMODULE_PROTOCOLS = %w[http https git ssh].freeze
+
# links to files listing for submodule if submodule is a project on this server
def submodule_links(submodule_item, ref = nil, repository = @repository)
url = repository.submodule_url_for(ref, submodule_item.path)
- return url, nil unless url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
-
- namespace = $1
- project = $2
- project.chomp!('.git')
+ if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
+ namespace, project = $1, $2
+ project.sub!(/\.git\z/, '')
- if self_url?(url, namespace, project)
- return namespace_project_path(namespace, project),
- namespace_project_tree_path(namespace, project,
- submodule_item.id)
- elsif relative_self_url?(url)
- relative_self_links(url, submodule_item.id)
- elsif github_dot_com_url?(url)
- standard_links('github.com', namespace, project, submodule_item.id)
- elsif gitlab_dot_com_url?(url)
- standard_links('gitlab.com', namespace, project, submodule_item.id)
+ if self_url?(url, namespace, project)
+ [namespace_project_path(namespace, project),
+ namespace_project_tree_path(namespace, project, submodule_item.id)]
+ elsif relative_self_url?(url)
+ relative_self_links(url, submodule_item.id)
+ elsif github_dot_com_url?(url)
+ standard_links('github.com', namespace, project, submodule_item.id)
+ elsif gitlab_dot_com_url?(url)
+ standard_links('gitlab.com', namespace, project, submodule_item.id)
+ else
+ [sanitize_submodule_url(url), nil]
+ end
else
- return url, nil
+ [sanitize_submodule_url(url), nil]
end
end
@@ -73,4 +75,16 @@ module SubmoduleHelper
namespace_project_tree_path(namespace, base, commit)
]
end
+
+ def sanitize_submodule_url(url)
+ uri = URI.parse(url)
+
+ if uri.scheme.in?(VALID_SUBMODULE_PROTOCOLS)
+ uri.to_s
+ else
+ nil
+ end
+ rescue URI::InvalidURIError
+ nil
+ end
end
diff --git a/app/models/readme_blob.rb b/app/models/readme_blob.rb
new file mode 100644
index 00000000000..1863a08f1de
--- /dev/null
+++ b/app/models/readme_blob.rb
@@ -0,0 +1,13 @@
+class ReadmeBlob < SimpleDelegator
+ attr_reader :repository
+
+ def initialize(blob, repository)
+ @repository = repository
+
+ super(blob)
+ end
+
+ def rendered_markup
+ repository.rendered_readme
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 0f645c9f1da..9d9d94c4486 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -518,7 +518,7 @@ class Repository
def readme
if head = tree(:head)
- head.readme
+ ReadmeBlob.new(head.readme, self)
end
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index abfbefdf9a0..882e2fa0594 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -152,18 +152,5 @@ class Snippet < ActiveRecord::Base
where(table[:content].matches(pattern))
end
-
- def accessible_to(user)
- return are_public unless user.present?
- return all if user.admin?
-
- where(
- 'visibility_level IN (:visibility_levels)
- OR author_id = :author_id
- OR project_id IN (:project_ids)',
- visibility_levels: [Snippet::PUBLIC, Snippet::INTERNAL],
- author_id: user.id,
- project_ids: user.authorized_projects.select(:id))
- end
end
end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index fe148b0ec65..c89b8eca9be 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -40,10 +40,7 @@ class Tree
readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name)
- git_repo = repository.raw_repository
- @readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path)
- @readme.load_all_data!(git_repo)
- @readme
+ @readme = repository.blob_at(sha, readme_path)
end
def trees
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 3a96836917e..cf8ff92617f 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -13,7 +13,7 @@ class ProjectSnippetPolicy < BasePolicy
can! :read_project_snippet
end
- if @subject.private? && @subject.project.team.member?(@user)
+ if @subject.project.team.member?(@user)
can! :read_project_snippet
end
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 4f161beea4d..85da0be6fff 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -7,7 +7,7 @@ module Search
end
def execute
- snippets = Snippet.accessible_to(current_user)
+ snippets = SnippetsFinder.new(current_user).execute
Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 78c5b0c1dda..c3f55ff821f 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -3,7 +3,7 @@
.diff-file.file-holder
.js-file-title.file-title
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_path(discussion)
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_path(discussion), show_toggle: false
.diff-content.code.js-syntax-highlight
%table
diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml
index 8e929538351..57e8c3ca1e1 100644
--- a/app/views/import/base/create.js.haml
+++ b/app/views/import/base/create.js.haml
@@ -10,4 +10,4 @@
- else
:plain
job = $("tr#repo_#{@repo_id}")
- job.find(".import-actions").html("<i class='fa fa-exclamation-circle'></i> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}")
+ job.find(".import-actions").html("<i class='fa fa-exclamation-circle'></i> Error saving project: #{escape_javascript(h(@project.errors.full_messages.join(',')))}")
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index c0d12cbc66e..cf09d9db6b7 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -2,9 +2,9 @@
%article.readme-holder
.pull-right
- if can?(current_user, :push_code, @project)
- = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme'
- .file-content.wiki
- = markup(readme.name, readme.data, rendered: @repository.rendered_readme)
+ = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.path)), class: 'light edit-project-readme'
+
+ = render 'projects/blob/viewer', viewer: readme.rich_viewer, viewer_url: namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.path), viewer: :rich, format: :json)
- else
.row-content-block.second-block.center
%h3.page-title
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
index 5326bb3e0cf..41187d5ce66 100644
--- a/app/views/projects/blob/_viewer.html.haml
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -2,8 +2,8 @@
- render_error = viewer.render_error
- load_asynchronously = local_assigns.fetch(:load_asynchronously, viewer.server_side?) && render_error.nil?
-- url = url_for(params.merge(viewer: viewer.type, format: :json)) if load_asynchronously
-.blob-viewer{ data: { type: viewer.type, url: url }, class: ('hidden' if hidden) }
+- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_asynchronously
+.blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) }
- if load_asynchronously
.text-center.prepend-top-default.append-bottom-default
= icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content')
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
index 190e7290303..4e46351bf8a 100644
--- a/app/views/projects/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -16,7 +16,8 @@
name: "issue[milestone_id]",
"v-if" => "issue.milestone" }
.dropdown
- %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: "issue", use_id: "true" },
+ %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: "issue", use_id: "true", default_no: "true" },
+ ":data-selected" => "milestoneTitle",
":data-issuable-id" => "issue.id",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
Milestone
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index b158a81471c..74255167352 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -51,7 +51,7 @@
%ul
%li.stage-header
%span.stage-name
- {{ __('ProjectLifecycle|Stage') }}
+ {{ s__('ProjectLifecycle|Stage') }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The phase of the development lifecycle."), "aria-hidden" => "true" }
%li.median-header
%span.stage-name
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index 7d6b3701f95..4e4fdb73ae3 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -1,4 +1,8 @@
-%i.fa.diff-toggle-caret.fa-fw
+- show_toggle = local_assigns.fetch(:show_toggle, true)
+
+- if show_toggle
+ %i.fa.diff-toggle-caret.fa-fw
+
- if defined?(blob) && blob && diff_file.submodule?
%span
= icon('archive fw')
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index 2cd8d03e30e..25a87411cac 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -10,7 +10,7 @@
.panel-body
%pre
:preserve
- #{sanitize_repo_path(@project, @project.import_error)}
+ #{h(sanitize_repo_path(@project, @project.import_error))}
= form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f|
= render "shared/import_form", f: f
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index 01599060844..2c2f64283f5 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,8 +1,8 @@
%article.file-holder.readme-holder
.js-file-title.file-title
= blob_icon readme.mode, readme.name
- = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, @path, readme.name)) do
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path)) do
%strong
= readme.name
- .file-content.wiki
- = markup(readme.name, readme.data)
+
+ = render 'projects/blob/viewer', viewer: readme.rich_viewer, viewer_url: namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path), viewer: :rich, format: :json)
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index fb0efd85dcd..68862206248 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -28,7 +28,7 @@
%h3 Clone your wiki
%pre.dark
:preserve
- git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
+ git clone #{ content_tag(:span, h(default_url_to_repo(@project_wiki)), class: 'clone')}
cd #{h @project_wiki.path}
%h3 Start Gollum and edit locally
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index c229d18903f..12d99c3ab4b 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -3,10 +3,10 @@
- has_button = button_path || project_select_button
.row.empty-state
- .pull-right.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
+ .col-xs-12
.svg-content
= render 'shared/empty_states/icons/issues.svg'
- .col-xs-12{ class: "#{'col-sm-6' if has_button}" }
+ .col-xs-12.text-center
.text-content
- if has_button && current_user
%h4
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index 00fb77bdb3b..5e2f4cf109d 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -1,8 +1,8 @@
.row.empty-state.labels
- .pull-right.col-xs-12.col-sm-6
+ .col-xs-12
.svg-content
= render 'shared/empty_states/icons/labels.svg'
- .col-xs-12.col-sm-6
+ .col-xs-12.text-center
.text-content
%h4 Labels can be applied to issues and merge requests to categorize them.
%p You can also star a label to make it a priority label.
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 7f2f99f3406..3e64f403b8b 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -3,10 +3,10 @@
- has_button = button_path || project_select_button
.row.empty-state.merge-requests
- .col-xs-12{ class: "#{'col-sm-6 pull-right' if has_button}" }
+ .col-xs-12
.svg-content
= render 'shared/empty_states/icons/merge_requests.svg'
- .col-xs-12{ class: "#{'col-sm-6' if has_button}" }
+ .col-xs-12.text-center
.text-content
- if has_button
%h4
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index f0d50828e2a..6750921338a 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -6,7 +6,7 @@
- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
- placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
+ placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- if project
%ul.dropdown-footer-list
- if can? current_user, :admin_milestone, project
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 3a66880e177..305d1c36a73 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -43,7 +43,7 @@
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
- = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
+ = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true }})
- if issuable.has_attribute?(:time_estimate)
#issuable-time-tracker.block
// Fallback while content is loading
diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb
new file mode 100644
index 00000000000..bfae0c77700
--- /dev/null
+++ b/app/workers/namespaceless_project_destroy_worker.rb
@@ -0,0 +1,43 @@
+# Worker to destroy projects that do not have a namespace
+#
+# It destroys everything it can without having the info about the namespace it
+# used to belong to. Projects in this state should be rare.
+# The worker will reject doing anything for projects that *do* have a
+# namespace. For those use ProjectDestroyWorker instead.
+class NamespacelessProjectDestroyWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def self.bulk_perform_async(args_list)
+ Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
+ end
+
+ def perform(project_id)
+ begin
+ project = Project.unscoped.find(project_id)
+ rescue ActiveRecord::RecordNotFound
+ return
+ end
+ return unless project.namespace_id.nil? # Reject doing anything for projects that *do* have a namespace
+
+ project.team.truncate
+
+ unlink_fork(project) if project.forked?
+
+ # Override Project#remove_pages for this instance so it doesn't do anything
+ def project.remove_pages
+ end
+
+ project.destroy!
+ end
+
+ private
+
+ def unlink_fork(project)
+ merge_requests = project.forked_from_project.merge_requests.opened.from_project(project)
+
+ merge_requests.update_all(state: 'closed')
+
+ project.forked_project_link.destroy
+ end
+end
diff --git a/changelogs/unreleased/30949-empty-states.yml b/changelogs/unreleased/30949-empty-states.yml
new file mode 100644
index 00000000000..bef87a954b7
--- /dev/null
+++ b/changelogs/unreleased/30949-empty-states.yml
@@ -0,0 +1,4 @@
+---
+title: Center all empty states
+merge_request:
+author:
diff --git a/changelogs/unreleased/31157-respect-project-features-in-wiki-search.yml b/changelogs/unreleased/31157-respect-project-features-in-wiki-search.yml
new file mode 100644
index 00000000000..721bb435a2e
--- /dev/null
+++ b/changelogs/unreleased/31157-respect-project-features-in-wiki-search.yml
@@ -0,0 +1,4 @@
+---
+title: Enforce project features when searching blobs and wikis
+merge_request:
+author:
diff --git a/changelogs/unreleased/31474-issue-boards-sidebar-milestone-dropdown-should-not-be-multi-select.yml b/changelogs/unreleased/31474-issue-boards-sidebar-milestone-dropdown-should-not-be-multi-select.yml
new file mode 100644
index 00000000000..88e79e3b6ea
--- /dev/null
+++ b/changelogs/unreleased/31474-issue-boards-sidebar-milestone-dropdown-should-not-be-multi-select.yml
@@ -0,0 +1,4 @@
+---
+title: Disallow multiple selections for Milestone dropdown
+merge_request: 11084
+author:
diff --git a/changelogs/unreleased/branch-name-escape.yml b/changelogs/unreleased/branch-name-escape.yml
new file mode 100644
index 00000000000..bf46235fd79
--- /dev/null
+++ b/changelogs/unreleased/branch-name-escape.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed branches dropdown rendering branch names as HTML
+merge_request:
+author:
diff --git a/changelogs/unreleased/bvl-markup-pipeline.yml b/changelogs/unreleased/bvl-markup-pipeline.yml
new file mode 100644
index 00000000000..d73bad03340
--- /dev/null
+++ b/changelogs/unreleased/bvl-markup-pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Make Asciidoc & other markup go through pipeline to prevent XSS
+merge_request:
+author:
diff --git a/changelogs/unreleased/bvl-validate-urls-in-markdown-using-uri.yml b/changelogs/unreleased/bvl-validate-urls-in-markdown-using-uri.yml
new file mode 100644
index 00000000000..03c4e531d73
--- /dev/null
+++ b/changelogs/unreleased/bvl-validate-urls-in-markdown-using-uri.yml
@@ -0,0 +1,4 @@
+---
+title: Validate URLs in markdown using URI to detect the host correctly
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-async-tree-readme.yml b/changelogs/unreleased/dm-async-tree-readme.yml
new file mode 100644
index 00000000000..fb1cfeb210a
--- /dev/null
+++ b/changelogs/unreleased/dm-async-tree-readme.yml
@@ -0,0 +1,4 @@
+---
+title: Load tree readme asynchronously
+merge_request:
+author:
diff --git a/changelogs/unreleased/hamlit-xss-fix.yml b/changelogs/unreleased/hamlit-xss-fix.yml
new file mode 100644
index 00000000000..ba4713846e9
--- /dev/null
+++ b/changelogs/unreleased/hamlit-xss-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fix for XSS in project import view caused by Hamlit filter usage.
+merge_request:
+author:
diff --git a/changelogs/unreleased/rs-sanitize-submodule-urls.yml b/changelogs/unreleased/rs-sanitize-submodule-urls.yml
new file mode 100644
index 00000000000..463b3695687
--- /dev/null
+++ b/changelogs/unreleased/rs-sanitize-submodule-urls.yml
@@ -0,0 +1,4 @@
+---
+title: Sanitize submodule URLs before linking to them in the file tree view
+merge_request:
+author:
diff --git a/changelogs/unreleased/snippets-finder-visibility.yml b/changelogs/unreleased/snippets-finder-visibility.yml
new file mode 100644
index 00000000000..fde2262cc8d
--- /dev/null
+++ b/changelogs/unreleased/snippets-finder-visibility.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor snippets finder & dont return internal snippets for external users
+merge_request:
+author:
diff --git a/changelogs/unreleased/snippets_visibility.yml b/changelogs/unreleased/snippets_visibility.yml
new file mode 100644
index 00000000000..4c10c6882ab
--- /dev/null
+++ b/changelogs/unreleased/snippets_visibility.yml
@@ -0,0 +1,4 @@
+---
+title: Fix snippets visibility for show action - external users can not see internal snippets
+merge_request:
+author:
diff --git a/changelogs/unreleased/tc-clean-pending-delete-projects.yml b/changelogs/unreleased/tc-clean-pending-delete-projects.yml
new file mode 100644
index 00000000000..31b43999c31
--- /dev/null
+++ b/changelogs/unreleased/tc-clean-pending-delete-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Add post-deploy migration to clean up projects in `pending_delete` state
+merge_request: 11044
+author:
diff --git a/changelogs/unreleased/tc-fix-private-subgroups-shown.yml b/changelogs/unreleased/tc-fix-private-subgroups-shown.yml
new file mode 100644
index 00000000000..82e03921854
--- /dev/null
+++ b/changelogs/unreleased/tc-fix-private-subgroups-shown.yml
@@ -0,0 +1,4 @@
+---
+title: "Do not show private groups on subgroups page if user doesn't have access to"
+merge_request:
+author:
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 433381e79d3..0ca1f565185 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -40,6 +40,7 @@
- [expire_build_instance_artifacts, 1]
- [group_destroy, 1]
- [irker, 1]
+ - [namespaceless_project_destroy, 1]
- [project_cache, 1]
- [project_destroy, 1]
- [project_export, 1]
diff --git a/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb b/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb
new file mode 100644
index 00000000000..2d242da9ef8
--- /dev/null
+++ b/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb
@@ -0,0 +1,47 @@
+# This is the counterpart of RequeuePendingDeleteProjects and cleans all
+# projects with `pending_delete = true` and that do not have a namespace.
+class CleanupNamespacelessPendingDeleteProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ @offset = 0
+
+ loop do
+ ids = pending_delete_batch
+
+ break if ids.empty?
+
+ args = ids.map { |id| Array(id) }
+
+ NamespacelessProjectDestroyWorker.bulk_perform_async(args)
+
+ @offset += 1
+ end
+ end
+
+ def down
+ # noop
+ end
+
+ private
+
+ def pending_delete_batch
+ connection.exec_query(find_batch).map{ |row| row['id'] }
+ end
+
+ BATCH_SIZE = 5000
+
+ def find_batch
+ projects = Arel::Table.new(:projects)
+ projects.project(projects[:id]).
+ where(projects[:pending_delete].eq(true)).
+ where(projects[:namespace_id].eq(nil)).
+ skip(@offset * BATCH_SIZE).
+ take(BATCH_SIZE).
+ to_sql
+ end
+end
diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md
index 21de7d18632..603fa4a8194 100644
--- a/doc/api/access_requests.md
+++ b/doc/api/access_requests.md
@@ -1,4 +1,4 @@
-# Group and project access requests
+# Group and project access requests API
>**Note:** This feature was introduced in GitLab 8.11
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
index 49930f01945..5ca766bf87d 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -1,4 +1,4 @@
-# Environments
+# Environments API
## List environments
diff --git a/doc/api/groups.md b/doc/api/groups.md
index bc61bfec9b9..2b3d8e125c8 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -1,4 +1,4 @@
-# Groups
+# Groups API
## List groups
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 1d43b1298b9..75794cc8d04 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -1,4 +1,4 @@
-# Issues
+# Issues API
Every API call to issues must be authenticated.
diff --git a/doc/api/keys.md b/doc/api/keys.md
index 3ace1040f38..376ac27df3a 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -1,4 +1,4 @@
-# Keys
+# Keys API
## Get SSH key with user by ID of an SSH key
diff --git a/doc/api/labels.md b/doc/api/labels.md
index 778348ea371..ec93cf50e7a 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -1,4 +1,4 @@
-# Labels
+# Labels API
## List labels
diff --git a/doc/api/members.md b/doc/api/members.md
index 3c661284f11..3234f833eae 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -1,4 +1,4 @@
-# Group and project members
+# Group and project members API
**Valid access levels**
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index dde855b2bd4..cb22b67f556 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -1,4 +1,4 @@
-# Merge requests
+# Merge requests API
## List merge requests
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index 43047917f77..3a2c398e355 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -1,4 +1,4 @@
-# Notification settings
+# Notification settings API
>**Note:** This feature was [introduced][ce-5632] in GitLab 8.12.
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 188fbe7447d..673cf02705d 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1,4 +1,4 @@
-# Projects
+# Projects API
### Project visibility level
diff --git a/doc/api/settings.md b/doc/api/settings.md
index d99695ca986..eefbdda42ce 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -1,4 +1,4 @@
-# Application settings
+# Application settings API
These API calls allow you to read and modify GitLab instance application
settings as appear in `/admin/application_settings`. You have to be an
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
index e09d930698e..fb8cf97896c 100644
--- a/doc/api/snippets.md
+++ b/doc/api/snippets.md
@@ -1,4 +1,4 @@
-# Snippets
+# Snippets API
> [Introduced][ce-6373] in GitLab 8.15.
diff --git a/doc/api/todos.md b/doc/api/todos.md
index 77667a57195..dd4c737b729 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -1,4 +1,4 @@
-# Todos
+# Todos API
> [Introduced][ce-3188] in GitLab 8.10.
diff --git a/doc/api/users.md b/doc/api/users.md
index 86027bcc05c..331f9a9b80b 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -1,4 +1,4 @@
-# Users
+# Users API
## List users
diff --git a/features/project/project.feature b/features/project/project.feature
index aa22401c88e..23817ef3ac9 100644
--- a/features/project/project.feature
+++ b/features/project/project.feature
@@ -18,6 +18,7 @@ Feature: Project
Then I should see the default project avatar
And I should not see the "Remove avatar" button
+ @javascript
Scenario: I should have readme on page
And I visit project "Shop" page
Then I should see project "Shop" README
diff --git a/features/project/source/markdown_render.feature b/features/project/source/markdown_render.feature
index fd583618dcf..fe4466ad241 100644
--- a/features/project/source/markdown_render.feature
+++ b/features/project/source/markdown_render.feature
@@ -19,12 +19,14 @@ Feature: Project Source Markdown Render
And I click on Gitlab API in README
Then I should see correct document rendered
+ @javascript
Scenario: I view README in markdown branch
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
And I click on Rake tasks in README
Then I should see correct directory rendered
+ @javascript
Scenario: I view README in markdown branch to see reference links to directory
Then I should see files from repository in markdown
And I should see rendered README which contains correct links
@@ -74,6 +76,7 @@ Feature: Project Source Markdown Render
And I click on Gitlab API in README
Then I should see correct document rendered for markdown branch
+ @javascript
Scenario: I browse directory from markdown branch
When I visit markdown branch
Then I should see files from repository in markdown branch
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 280d70925f7..9c2196a8ef7 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -2,6 +2,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedPaths
+ include WaitForAjax
step 'change project settings' do
fill_in 'project_name_edit', with: 'NewName'
@@ -86,6 +87,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should see project "Shop" README' do
+ wait_for_ajax
page.within('.readme-holder') do
expect(page).to have_content 'testme'
end
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index abdbd795cd5..ada0ff20585 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -120,6 +120,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
When 'I visit markdown branch' do
visit namespace_project_tree_path(@project.namespace, @project, "markdown")
+ wait_for_ajax
end
When 'I visit markdown branch "README.md" blob' do
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 450b2f8d9a9..3da7d735da8 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -52,7 +52,7 @@ module API
elsif current_user.admin
Group.all
elsif params[:all_available]
- GroupsFinder.new.execute(current_user)
+ GroupsFinder.new(current_user).execute
else
current_user.groups
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 4caa210b9f2..226a7ddd50e 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -91,8 +91,8 @@ module API
end
def find_project_snippet(id)
- finder_params = { filter: :by_project, project: user_project }
- SnippetsFinder.new.execute(current_user, finder_params).find(id)
+ finder_params = { project: user_project }
+ SnippetsFinder.new(current_user, finder_params).execute.find(id)
end
def find_merge_request_with_access(iid, access_level = :read_merge_request)
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index cfee38a9baf..98bc9c28527 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -17,8 +17,7 @@ module API
end
def snippets_for_current_user
- finder_params = { filter: :by_project, project: user_project }
- SnippetsFinder.new.execute(current_user, finder_params)
+ SnippetsFinder.new(current_user, project: user_project).execute
end
end
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index b93fdc62808..53f5953a8fb 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -8,11 +8,11 @@ module API
resource :snippets do
helpers do
def snippets_for_current_user
- SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ SnippetsFinder.new(current_user, author: current_user).execute
end
def public_snippets
- SnippetsFinder.new.execute(current_user, filter: :public)
+ SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
end
end
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
index 0a63cfd3df1..6187445fc8d 100644
--- a/lib/api/v3/groups.rb
+++ b/lib/api/v3/groups.rb
@@ -45,7 +45,7 @@ module API
groups = if current_user.admin
Group.all
elsif params[:all_available]
- GroupsFinder.new.execute(current_user)
+ GroupsFinder.new(current_user).execute
else
current_user.groups
end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
index fc065a22d74..c41fee32610 100644
--- a/lib/api/v3/project_snippets.rb
+++ b/lib/api/v3/project_snippets.rb
@@ -18,8 +18,7 @@ module API
end
def snippets_for_current_user
- finder_params = { filter: :by_project, project: user_project }
- SnippetsFinder.new.execute(current_user, finder_params)
+ SnippetsFinder.new(current_user, project: user_project).execute
end
end
diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb
index 07dac7e9904..0762fc02d70 100644
--- a/lib/api/v3/snippets.rb
+++ b/lib/api/v3/snippets.rb
@@ -8,11 +8,11 @@ module API
resource :snippets do
helpers do
def snippets_for_current_user
- SnippetsFinder.new.execute(current_user, filter: :by_user, user: current_user)
+ SnippetsFinder.new(current_user, author: current_user).execute
end
def public_snippets
- SnippetsFinder.new.execute(current_user, filter: :public)
+ SnippetsFinder.new(current_user, visibility: Snippet::PUBLIC).execute
end
end
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index d67d466bce8..7d15a0f6d44 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -2,16 +2,17 @@ module Banzai
module Filter
# HTML Filter to modify the attributes of external links
class ExternalLinkFilter < HTML::Pipeline::Filter
+ SCHEMES = ['http', 'https', nil].freeze
+
def call
links.each do |node|
- href = href_to_lowercase_scheme(node["href"].to_s)
+ uri = uri(node['href'].to_s)
+ next unless uri
- unless node["href"].to_s == href
- node.set_attribute('href', href)
- end
+ node.set_attribute('href', uri.to_s)
- if href =~ %r{\A(https?:)?//[^/]} && external_url?(href)
- node.set_attribute('rel', 'nofollow noreferrer')
+ if SCHEMES.include?(uri.scheme) && external_url?(uri)
+ node.set_attribute('rel', 'nofollow noreferrer noopener')
node.set_attribute('target', '_blank')
end
end
@@ -21,27 +22,26 @@ module Banzai
private
+ def uri(href)
+ URI.parse(href)
+ rescue URI::InvalidURIError
+ nil
+ end
+
def links
query = 'descendant-or-self::a[@href and not(@href = "")]'
doc.xpath(query)
end
- def href_to_lowercase_scheme(href)
- scheme_match = href.match(/\A(\w+):\/\//)
-
- if scheme_match
- scheme_match.to_s.downcase + scheme_match.post_match
- else
- href
- end
- end
+ def external_url?(uri)
+ # Relative URLs miss a hostname
+ return false unless uri.hostname
- def external_url?(url)
- !url.start_with?(internal_url)
+ uri.hostname != internal_url.hostname
end
def internal_url
- @internal_url ||= Gitlab.config.gitlab.url
+ @internal_url ||= URI.parse(Gitlab.config.gitlab.url)
end
end
end
diff --git a/lib/banzai/pipeline/markup_pipeline.rb b/lib/banzai/pipeline/markup_pipeline.rb
new file mode 100644
index 00000000000..c56d908009f
--- /dev/null
+++ b/lib/banzai/pipeline/markup_pipeline.rb
@@ -0,0 +1,13 @@
+module Banzai
+ module Pipeline
+ class MarkupPipeline < BasePipeline
+ def self.filters
+ @filters ||= FilterArray[
+ Filter::SanitizationFilter,
+ Filter::ExternalLinkFilter,
+ Filter::PlantumlFilter
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index fba80c7132e..96d38f6daa0 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -15,17 +15,17 @@ module Gitlab
#
# input - the source text in Asciidoc format
#
- def self.render(input)
+ def self.render(input, context)
asciidoc_opts = { safe: :secure,
backend: :gitlab_html5,
attributes: DEFAULT_ADOC_ATTRS }
+ context[:pipeline] = :markup
+
plantuml_setup
html = ::Asciidoctor.convert(input, asciidoc_opts)
-
- filter = Banzai::Filter::SanitizationFilter.new(html)
- html = filter.call.to_s
+ html = Banzai.render(html, context)
html.html_safe
end
diff --git a/lib/gitlab/other_markup.rb b/lib/gitlab/other_markup.rb
index c2adc9aa10b..31a24460f0f 100644
--- a/lib/gitlab/other_markup.rb
+++ b/lib/gitlab/other_markup.rb
@@ -5,12 +5,12 @@ module Gitlab
#
# input - the source text in a markup format
#
- def self.render(file_name, input)
+ def self.render(file_name, input, context)
html = GitHub::Markup.render(file_name, input).
force_encoding(input.encoding)
+ context[:pipeline] = :markup
- filter = Banzai::Filter::SanitizationFilter.new(html)
- html = filter.call.to_s
+ html = Banzai.render(html, context)
html.html_safe
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 0b8959f2fb9..47cfe412715 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -82,6 +82,8 @@ module Gitlab
private
def blobs
+ return [] unless Ability.allowed?(@current_user, :download_code, @project)
+
@blobs ||= begin
blobs = project.repository.search_files_by_content(query, repository_ref).first(100)
found_file_names = Set.new
@@ -102,6 +104,8 @@ module Gitlab
end
def wiki_blobs
+ return [] unless Ability.allowed?(@current_user, :read_wiki, @project)
+
@wiki_blobs ||= begin
if project.wiki_enabled? && query.present?
project_wiki = ProjectWiki.new(project)
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 765d2bdbf7a..df8ea225814 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -26,6 +26,41 @@ describe GroupsController do
end
end
+ describe 'GET #subgroups' do
+ let!(:public_subgroup) { create(:group, :public, parent: group) }
+ let!(:private_subgroup) { create(:group, :private, parent: group) }
+
+ context 'as a user' do
+ before do
+ sign_in(user)
+ end
+
+ it 'shows the public subgroups' do
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup)
+ end
+
+ context 'being member' do
+ it 'shows public and private subgroups the user is member of' do
+ private_subgroup.add_guest(user)
+
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup, private_subgroup)
+ end
+ end
+ end
+
+ context 'as a guest' do
+ it 'shows the public subgroups' do
+ get :subgroups, id: group.to_param
+
+ expect(assigns(:nested_groups)).to contain_exactly(public_subgroup)
+ end
+ end
+ end
+
describe 'GET #issues' do
let(:issue_1) { create(:issue, project: project) }
let(:issue_2) { create(:issue, project: project) }
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 41cd5bdcdd8..930415a4778 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -3,6 +3,34 @@ require 'spec_helper'
describe SnippetsController do
let(:user) { create(:user) }
+ describe 'GET #index' do
+ let(:user) { create(:user) }
+
+ context 'when username parameter is present' do
+ it 'renders snippets of a user when username is present' do
+ get :index, username: user.username
+
+ expect(response).to render_template(:index)
+ end
+ end
+
+ context 'when username parameter is not present' do
+ it 'redirects to explore snippets page when user is not logged in' do
+ get :index
+
+ expect(response).to redirect_to(explore_snippets_path)
+ end
+
+ it 'redirects to snippets dashboard page when user is logged in' do
+ sign_in(user)
+
+ get :index
+
+ expect(response).to redirect_to(dashboard_snippets_path)
+ end
+ end
+ end
+
describe 'GET #new' do
context 'when signed in' do
before do
@@ -132,7 +160,7 @@ describe SnippetsController do
it 'responds with status 404' do
get :show, id: 'doesntexist'
- expect(response).to have_http_status(404)
+ expect(response).to redirect_to(new_user_session_path)
end
end
end
@@ -478,10 +506,10 @@ describe SnippetsController do
end
context 'when not signed in' do
- it 'responds with status 404' do
+ it 'redirects to the sign in path' do
get :raw, id: 'doesntexist'
- expect(response).to have_http_status(404)
+ expect(response).to redirect_to(new_user_session_path)
end
end
end
diff --git a/spec/features/dashboard/milestone_filter_spec.rb b/spec/features/dashboard/milestone_filter_spec.rb
new file mode 100644
index 00000000000..628627f70d4
--- /dev/null
+++ b/spec/features/dashboard/milestone_filter_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe 'Dashboard > milestone filter', feature: true, js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+ let(:milestone) { create(:milestone, title: "v1.0", project: project) }
+ let(:milestone2) { create(:milestone, title: "v2.0", project: project) }
+ let!(:issue) { create :issue, author: user, project: project, milestone: milestone }
+ let!(:issue2) { create :issue, author: user, project: project, milestone: milestone2 }
+
+ before do
+ login_as(user)
+ visit issues_dashboard_path(author_id: user.id)
+ end
+
+ context 'default state' do
+ it 'shows issues with Any Milestone' do
+ page.all('.issue-info').each do |issue_info|
+ expect(issue_info.text).to match(/v\d.0/)
+ end
+ end
+ end
+
+ context 'filtering by milestone' do
+ milestone_select = '.js-milestone-select'
+
+ before do
+ find(milestone_select).click
+
+ page.within('.dropdown-content') do
+ click_link 'v1.0'
+ end
+
+ find(milestone_select).click
+ end
+
+ it 'shows issues with Milestone v1.0' do
+ expect(find('.issues-list')).to have_selector('.issue', count: 1)
+
+ find(milestone_select).click
+
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+ end
+
+ it 'should not change active Milestone unless clicked' do
+ find(milestone_select).click
+
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+
+ # open & close dropdown
+ find('.dropdown-menu-close').click
+ expect(find('.milestone-filter')).not_to have_selector('.dropdown.open')
+ find(milestone_select).click
+
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+ expect(find('.dropdown-content a.is-active')).to have_content('v1.0')
+ end
+ end
+end
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index 62937688c22..c6ba118220a 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -12,4 +12,51 @@ describe 'Dashboard snippets', feature: true do
it_behaves_like 'paginated snippets'
end
+
+ context 'filtering by visibility' do
+ let(:user) { create(:user) }
+ let!(:snippets) do
+ [
+ create(:personal_snippet, :public, author: user),
+ create(:personal_snippet, :internal, author: user),
+ create(:personal_snippet, :private, author: user),
+ create(:personal_snippet, :public)
+ ]
+ end
+
+ before do
+ login_as(user)
+
+ visit dashboard_snippets_path
+ end
+
+ it 'contains all snippets of logged user' do
+ expect(page).to have_selector('.snippet-row', count: 3)
+
+ expect(page).to have_content(snippets[0].title)
+ expect(page).to have_content(snippets[1].title)
+ expect(page).to have_content(snippets[2].title)
+ end
+
+ it 'contains all private snippets of logged user when clicking on private' do
+ click_link('Private')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[2].title)
+ end
+
+ it 'contains all internal snippets of logged user when clicking on internal' do
+ click_link('Internal')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[1].title)
+ end
+
+ it 'contains all public snippets of logged user when clicking on public' do
+ click_link('Public')
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(snippets[0].title)
+ end
+ end
end
diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb
index d37e8ed4699..18689c17fe9 100644
--- a/spec/features/projects/snippets_spec.rb
+++ b/spec/features/projects/snippets_spec.rb
@@ -4,11 +4,27 @@ describe 'Project snippets', feature: true do
context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
- before do
- allow(Snippet).to receive(:default_per_page).and_return(1)
- visit namespace_project_snippets_path(project.namespace, project)
+ let!(:other_snippet) { create(:project_snippet) }
+
+ context 'pagination' do
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+
+ visit namespace_project_snippets_path(project.namespace, project)
+ end
+
+ it_behaves_like 'paginated snippets'
end
- it_behaves_like 'paginated snippets'
+ context 'list content' do
+ it 'contains all project snippets' do
+ visit namespace_project_snippets_path(project.namespace, project)
+
+ expect(page).to have_selector('.snippet-row', count: 2)
+
+ expect(page).to have_content(snippets[0].title)
+ expect(page).to have_content(snippets[1].title)
+ end
+ end
end
end
diff --git a/spec/features/snippets/explore_spec.rb b/spec/features/snippets/explore_spec.rb
index 10a4597e467..fd097fe2e74 100644
--- a/spec/features/snippets/explore_spec.rb
+++ b/spec/features/snippets/explore_spec.rb
@@ -1,11 +1,11 @@
require 'rails_helper'
feature 'Explore Snippets', feature: true do
- scenario 'User should see snippets that are not private' do
- public_snippet = create(:personal_snippet, :public)
- internal_snippet = create(:personal_snippet, :internal)
- private_snippet = create(:personal_snippet, :private)
+ let!(:public_snippet) { create(:personal_snippet, :public) }
+ let!(:internal_snippet) { create(:personal_snippet, :internal) }
+ let!(:private_snippet) { create(:personal_snippet, :private) }
+ scenario 'User should see snippets that are not private' do
login_as create(:user)
visit explore_snippets_path
@@ -13,4 +13,21 @@ feature 'Explore Snippets', feature: true do
expect(page).to have_content(internal_snippet.title)
expect(page).not_to have_content(private_snippet.title)
end
+
+ scenario 'External user should see only public snippets' do
+ login_as create(:user, :external)
+ visit explore_snippets_path
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
+
+ scenario 'Not authenticated user should see only public snippets' do
+ visit explore_snippets_path
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
end
diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb
new file mode 100644
index 00000000000..93382f4c359
--- /dev/null
+++ b/spec/features/snippets/internal_snippet_spec.rb
@@ -0,0 +1,23 @@
+require 'rails_helper'
+
+feature 'Internal Snippets', feature: true, js: true do
+ let(:internal_snippet) { create(:personal_snippet, :internal) }
+
+ describe 'normal user' do
+ before do
+ login_as :user
+ end
+
+ scenario 'sees internal snippets' do
+ visit snippet_path(internal_snippet)
+
+ expect(page).to have_content(internal_snippet.content)
+ end
+
+ scenario 'sees raw internal snippets' do
+ visit raw_snippet_path(internal_snippet)
+
+ expect(page).to have_content(internal_snippet.content)
+ end
+ end
+end
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index 1546a06b80c..4efbd672322 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -3,14 +3,46 @@ require 'spec_helper'
describe 'Snippets tab on a user profile', feature: true, js: true do
context 'when the user has snippets' do
let(:user) { create(:user) }
- let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
- before do
- allow(Snippet).to receive(:default_per_page).and_return(1)
- visit user_path(user)
- page.within('.user-profile-nav') { click_link 'Snippets' }
- wait_for_ajax
+
+ context 'pagination' do
+ let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
+
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+ end
+
+ it_behaves_like 'paginated snippets', remote: true
end
- it_behaves_like 'paginated snippets', remote: true
+ context 'list content' do
+ let!(:public_snippet) { create(:snippet, :public, author: user) }
+ let!(:internal_snippet) { create(:snippet, :internal, author: user) }
+ let!(:private_snippet) { create(:snippet, :private, author: user) }
+ let!(:other_snippet) { create(:snippet, :public) }
+
+ it 'contains only internal and public snippets of a user when a user is logged in' do
+ login_as(:user)
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+
+ expect(page).to have_selector('.snippet-row', count: 2)
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).to have_content(internal_snippet.title)
+ end
+
+ it 'contains only public snippets of a user when a user is not logged in' do
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+
+ expect(page).to have_selector('.snippet-row', count: 1)
+ expect(page).to have_content(public_snippet.title)
+ end
+ end
end
end
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
index d5d111e8d15..5b3591550c1 100644
--- a/spec/finders/groups_finder_spec.rb
+++ b/spec/finders/groups_finder_spec.rb
@@ -3,29 +3,64 @@ require 'spec_helper'
describe GroupsFinder do
describe '#execute' do
let(:user) { create(:user) }
- let!(:private_group) { create(:group, :private) }
- let!(:internal_group) { create(:group, :internal) }
- let!(:public_group) { create(:group, :public) }
- let(:finder) { described_class.new }
- describe 'execute' do
- describe 'without a user' do
- subject { finder.execute }
+ context 'root level groups' do
+ let!(:private_group) { create(:group, :private) }
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:public_group) { create(:group, :public) }
+
+ context 'without a user' do
+ subject { described_class.new.execute }
it { is_expected.to eq([public_group]) }
end
- describe 'with a user' do
- subject { finder.execute(user) }
+ context 'with a user' do
+ subject { described_class.new(user).execute }
context 'normal user' do
- it { is_expected.to eq([public_group, internal_group]) }
+ it { is_expected.to contain_exactly(public_group, internal_group) }
end
context 'external user' do
let(:user) { create(:user, external: true) }
- it { is_expected.to eq([public_group]) }
+ it { is_expected.to contain_exactly(public_group) }
+ end
+
+ context 'user is member of the private group' do
+ before do
+ private_group.add_guest(user)
+ end
+
+ it { is_expected.to contain_exactly(public_group, internal_group, private_group) }
+ end
+ end
+ end
+
+ context 'subgroups' do
+ let!(:parent_group) { create(:group, :public) }
+ let!(:public_subgroup) { create(:group, :public, parent: parent_group) }
+ let!(:internal_subgroup) { create(:group, :internal, parent: parent_group) }
+ let!(:private_subgroup) { create(:group, :private, parent: parent_group) }
+
+ context 'without a user' do
+ it 'only returns public subgroups' do
+ expect(described_class.new(nil, parent: parent_group).execute).to contain_exactly(public_subgroup)
+ end
+ end
+
+ context 'with a user' do
+ it 'returns public and internal subgroups' do
+ expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup)
+ end
+
+ context 'being member' do
+ it 'returns public subgroups, internal subgroups, and private subgroups user is member of' do
+ private_subgroup.add_guest(user)
+
+ expect(described_class.new(user, parent: parent_group).execute).to contain_exactly(public_subgroup, internal_subgroup, private_subgroup)
+ end
end
end
end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index cb6c80d1bd0..35f1683eef9 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -8,79 +8,145 @@ describe SnippetsFinder do
let(:project1) { create(:empty_project, :public, group: group) }
let(:project2) { create(:empty_project, :private, group: group) }
- context ':all filter' do
+ context 'all snippets visible to a user' do
let!(:snippet1) { create(:personal_snippet, :private) }
let!(:snippet2) { create(:personal_snippet, :internal) }
let!(:snippet3) { create(:personal_snippet, :public) }
+ let!(:project_snippet1) { create(:project_snippet, :private) }
+ let!(:project_snippet2) { create(:project_snippet, :internal) }
+ let!(:project_snippet3) { create(:project_snippet, :public) }
it "returns all private and internal snippets" do
- snippets = described_class.new.execute(user, filter: :all)
- expect(snippets).to include(snippet2, snippet3)
- expect(snippets).not_to include(snippet1)
+ snippets = described_class.new(user, scope: :all).execute
+ expect(snippets).to include(snippet2, snippet3, project_snippet2, project_snippet3)
+ expect(snippets).not_to include(snippet1, project_snippet1)
end
it "returns all public snippets" do
- snippets = described_class.new.execute(nil, filter: :all)
- expect(snippets).to include(snippet3)
- expect(snippets).not_to include(snippet1, snippet2)
+ snippets = described_class.new(nil, scope: :all).execute
+ expect(snippets).to include(snippet3, project_snippet3)
+ expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2)
+ end
+
+ it "returns all public and internal snippets for normal user" do
+ snippets = described_class.new(user).execute
+
+ expect(snippets).to include(snippet2, snippet3, project_snippet2, project_snippet3)
+ expect(snippets).not_to include(snippet1, project_snippet1)
+ end
+
+ it "returns all public snippets for non authorized user" do
+ snippets = described_class.new(nil).execute
+
+ expect(snippets).to include(snippet3, project_snippet3)
+ expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2)
+ end
+
+ it "returns all public and authored snippets for external user" do
+ external_user = create(:user, :external)
+ authored_snippet = create(:personal_snippet, :internal, author: external_user)
+
+ snippets = described_class.new(external_user).execute
+
+ expect(snippets).to include(snippet3, project_snippet3, authored_snippet)
+ expect(snippets).not_to include(snippet1, snippet2, project_snippet1, project_snippet2)
end
end
- context ':public filter' do
+ context 'filter by visibility' do
let!(:snippet1) { create(:personal_snippet, :private) }
let!(:snippet2) { create(:personal_snippet, :internal) }
let!(:snippet3) { create(:personal_snippet, :public) }
- it "returns public public snippets" do
- snippets = described_class.new.execute(nil, filter: :public)
+ it "returns public snippets when visibility is PUBLIC" do
+ snippets = described_class.new(nil, visibility: Snippet::PUBLIC).execute
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
end
end
- context ':by_user filter' do
+ context 'filter by scope' do
+ let!(:snippet1) { create(:personal_snippet, :private, author: user) }
+ let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
+ let!(:snippet3) { create(:personal_snippet, :public, author: user) }
+
+ it "returns all snippets for 'all' scope" do
+ snippets = described_class.new(user, scope: :all).execute
+
+ expect(snippets).to include(snippet1, snippet2, snippet3)
+ end
+
+ it "returns all snippets for 'are_private' scope" do
+ snippets = described_class.new(user, scope: :are_private).execute
+
+ expect(snippets).to include(snippet1)
+ expect(snippets).not_to include(snippet2, snippet3)
+ end
+
+ it "returns all snippets for 'are_interna;' scope" do
+ snippets = described_class.new(user, scope: :are_internal).execute
+
+ expect(snippets).to include(snippet2)
+ expect(snippets).not_to include(snippet1, snippet3)
+ end
+
+ it "returns all snippets for 'are_private' scope" do
+ snippets = described_class.new(user, scope: :are_public).execute
+
+ expect(snippets).to include(snippet3)
+ expect(snippets).not_to include(snippet1, snippet2)
+ end
+ end
+
+ context 'filter by author' do
let!(:snippet1) { create(:personal_snippet, :private, author: user) }
let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
let!(:snippet3) { create(:personal_snippet, :public, author: user) }
it "returns all public and internal snippets" do
- snippets = described_class.new.execute(user1, filter: :by_user, user: user)
+ snippets = described_class.new(user1, author: user).execute
+
expect(snippets).to include(snippet2, snippet3)
expect(snippets).not_to include(snippet1)
end
it "returns internal snippets" do
- snippets = described_class.new.execute(user, filter: :by_user, user: user, scope: "are_internal")
+ snippets = described_class.new(user, author: user, visibility: Snippet::INTERNAL).execute
+
expect(snippets).to include(snippet2)
expect(snippets).not_to include(snippet1, snippet3)
end
it "returns private snippets" do
- snippets = described_class.new.execute(user, filter: :by_user, user: user, scope: "are_private")
+ snippets = described_class.new(user, author: user, visibility: Snippet::PRIVATE).execute
+
expect(snippets).to include(snippet1)
expect(snippets).not_to include(snippet2, snippet3)
end
it "returns public snippets" do
- snippets = described_class.new.execute(user, filter: :by_user, user: user, scope: "are_public")
+ snippets = described_class.new(user, author: user, visibility: Snippet::PUBLIC).execute
+
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet1, snippet2)
end
it "returns all snippets" do
- snippets = described_class.new.execute(user, filter: :by_user, user: user)
+ snippets = described_class.new(user, author: user).execute
+
expect(snippets).to include(snippet1, snippet2, snippet3)
end
it "returns only public snippets if unauthenticated user" do
- snippets = described_class.new.execute(nil, filter: :by_user, user: user)
+ snippets = described_class.new(nil, author: user).execute
+
expect(snippets).to include(snippet3)
expect(snippets).not_to include(snippet2, snippet1)
end
end
- context 'by_project filter' do
+ context 'filter by project' do
before do
@snippet1 = create(:project_snippet, :private, project: project1)
@snippet2 = create(:project_snippet, :internal, project: project1)
@@ -88,43 +154,52 @@ describe SnippetsFinder do
end
it "returns public snippets for unauthorized user" do
- snippets = described_class.new.execute(nil, filter: :by_project, project: project1)
+ snippets = described_class.new(nil, project: project1).execute
+
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet1, @snippet2)
end
it "returns public and internal snippets for non project members" do
- snippets = described_class.new.execute(user, filter: :by_project, project: project1)
+ snippets = described_class.new(user, project: project1).execute
+
expect(snippets).to include(@snippet2, @snippet3)
expect(snippets).not_to include(@snippet1)
end
it "returns public snippets for non project members" do
- snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_public")
+ snippets = described_class.new(user, project: project1, visibility: Snippet::PUBLIC).execute
+
expect(snippets).to include(@snippet3)
expect(snippets).not_to include(@snippet1, @snippet2)
end
it "returns internal snippets for non project members" do
- snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_internal")
+ snippets = described_class.new(user, project: project1, visibility: Snippet::INTERNAL).execute
+
expect(snippets).to include(@snippet2)
expect(snippets).not_to include(@snippet1, @snippet3)
end
it "does not return private snippets for non project members" do
- snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+ snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+
expect(snippets).not_to include(@snippet1, @snippet2, @snippet3)
end
it "returns all snippets for project members" do
project1.team << [user, :developer]
- snippets = described_class.new.execute(user, filter: :by_project, project: project1)
+
+ snippets = described_class.new(user, project: project1).execute
+
expect(snippets).to include(@snippet1, @snippet2, @snippet3)
end
it "returns private snippets for project members" do
project1.team << [user, :developer]
- snippets = described_class.new.execute(user, filter: :by_project, project: project1, scope: "are_private")
+
+ snippets = described_class.new(user, project: project1, visibility: Snippet::PRIVATE).execute
+
expect(snippets).to include(@snippet1)
end
end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 345bc33a67b..9da33792659 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -109,6 +109,18 @@ describe SubmoduleHelper do
end
context 'submodule on unsupported' do
+ it 'sanitizes unsupported protocols' do
+ stub_url('javascript:alert("XSS");')
+
+ expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ end
+
+ it 'sanitizes unsupported protocols disguised as a repository URL' do
+ stub_url('javascript:alert("XSS");foo/bar.git')
+
+ expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ end
+
it 'returns original' do
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index d9e4525cb28..6f6c215be87 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -1,5 +1,22 @@
require 'spec_helper'
+shared_examples 'an external link with rel attribute' do
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
+
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
+
+ it 'adds rel="noopener" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noopener'
+ end
+end
+
describe Banzai::Filter::ExternalLinkFilter, lib: true do
include FilterSpecHelper
@@ -22,49 +39,51 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
context 'for root links on document' do
let(:doc) { filter %q(<a href="https://google.com/">Google</a>) }
- it 'adds rel="nofollow" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
- end
-
- it 'adds rel="noreferrer" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
- end
+ it_behaves_like 'an external link with rel attribute'
end
context 'for nested links on document' do
let(:doc) { filter %q(<p><a href="https://google.com/">Google</a></p>) }
- it 'adds rel="nofollow" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
+ it_behaves_like 'an external link with rel attribute'
+ end
+
+ context 'for invalid urls' do
+ it 'skips broken hrefs' do
+ doc = filter %q(<p><a href="don't crash on broken urls">Google</a></p>)
+ expected = %q(<p><a href="don't%20crash%20on%20broken%20urls">Google</a></p>)
+
+ expect(doc.to_html).to eq(expected)
end
+ end
+
+ context 'for links with a username' do
+ context 'with a valid username' do
+ let(:doc) { filter %q(<a href="https://user@google.com/">Google</a>) }
- it 'adds rel="noreferrer" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ it_behaves_like 'an external link with rel attribute'
+ end
+
+ context 'with an impersonated username' do
+ let(:internal) { Gitlab.config.gitlab.url }
+
+ let(:doc) { filter %Q(<a href="https://#{internal}@example.com" target="_blank">Reverse Tabnabbing</a>) }
+
+ it_behaves_like 'an external link with rel attribute'
end
end
context 'for non-lowercase scheme links' do
- let(:doc_with_http) { filter %q(<p><a href="httP://google.com/">Google</a></p>) }
- let(:doc_with_https) { filter %q(<p><a href="hTTpS://google.com/">Google</a></p>) }
-
- it 'adds rel="nofollow" to external links' do
- expect(doc_with_http.at_css('a')).to have_attribute('rel')
- expect(doc_with_https.at_css('a')).to have_attribute('rel')
+ context 'with http' do
+ let(:doc) { filter %q(<p><a href="httP://google.com/">Google</a></p>) }
- expect(doc_with_http.at_css('a')['rel']).to include 'nofollow'
- expect(doc_with_https.at_css('a')['rel']).to include 'nofollow'
+ it_behaves_like 'an external link with rel attribute'
end
- it 'adds rel="noreferrer" to external links' do
- expect(doc_with_http.at_css('a')).to have_attribute('rel')
- expect(doc_with_https.at_css('a')).to have_attribute('rel')
+ context 'with https' do
+ let(:doc) { filter %q(<p><a href="hTTpS://google.com/">Google</a></p>) }
- expect(doc_with_http.at_css('a')['rel']).to include 'noreferrer'
- expect(doc_with_https.at_css('a')['rel']).to include 'noreferrer'
+ it_behaves_like 'an external link with rel attribute'
end
it 'skips internal links' do
@@ -84,14 +103,6 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
context 'for protocol-relative links' do
let(:doc) { filter %q(<p><a href="//google.com/">Google</a></p>) }
- it 'adds rel="nofollow" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
- end
-
- it 'adds rel="noreferrer" to external links' do
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
- end
+ it_behaves_like 'an external link with rel attribute'
end
end
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 0f47fb2fbd9..2c7ebb15fd7 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -22,7 +22,22 @@ module Gitlab
expect(Asciidoctor).to receive(:convert)
.with(input, expected_asciidoc_opts).and_return(html)
- expect(render(input)).to eq(html)
+ expect(render(input, context)).to eq(html)
+ end
+
+ context "with asciidoc_opts" do
+ it "merges the options with default ones" do
+ expected_asciidoc_opts = {
+ safe: :secure,
+ backend: :gitlab_html5,
+ attributes: described_class::DEFAULT_ADOC_ATTRS
+ }
+
+ expect(Asciidoctor).to receive(:convert)
+ .with(input, expected_asciidoc_opts).and_return(html)
+
+ render(input, context)
+ end
end
context "XSS" do
@@ -33,7 +48,7 @@ module Gitlab
},
'images' => {
input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
- output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt=\"Alt text\"></span></p>\n</div>"
+ output: "<img src=\"https://localhost.com/image.png\" alt=\"Alt text\">"
},
'pre' => {
input: '```mypre"><script>alert(3)</script>',
@@ -43,10 +58,18 @@ module Gitlab
links.each do |name, data|
it "does not convert dangerous #{name} into HTML" do
- expect(render(data[:input])).to eq(data[:output])
+ expect(render(data[:input], context)).to include(data[:output])
end
end
end
+
+ context 'external links' do
+ it 'adds the `rel` attribute to the link' do
+ output = render('link:https://google.com[Google]', context)
+
+ expect(output).to include('rel="nofollow noreferrer noopener"')
+ end
+ end
end
def render(*args)
diff --git a/spec/lib/gitlab/other_markup_spec.rb b/spec/lib/gitlab/other_markup_spec.rb
index d6d53e8586c..c0f5fa9dc1f 100644
--- a/spec/lib/gitlab/other_markup_spec.rb
+++ b/spec/lib/gitlab/other_markup_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::OtherMarkup, lib: true do
}
links.each do |name, data|
it "does not convert dangerous #{name} into HTML" do
- expect(render(data[:file], data[:input])).to eq(data[:output])
+ expect(render(data[:file], data[:input], context)).to eq(data[:output])
end
end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index a7c8e7f1f57..6e0b1192706 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -22,8 +22,37 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
describe 'blob search' do
- let(:project) { create(:project, :repository) }
- let(:results) { described_class.new(user, project, 'files').objects('blobs') }
+ let(:project) { create(:project, :public, :repository) }
+
+ subject(:results) { described_class.new(user, project, 'files').objects('blobs') }
+
+ context 'when repository is disabled' do
+ let(:project) { create(:project, :public, :repository, :repository_disabled) }
+
+ it 'hides blobs from members' do
+ project.add_reporter(user)
+
+ is_expected.to be_empty
+ end
+
+ it 'hides blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when repository is internal' do
+ let(:project) { create(:project, :public, :repository, :repository_private) }
+
+ it 'finds blobs for members' do
+ project.add_reporter(user)
+
+ is_expected.not_to be_empty
+ end
+
+ it 'hides blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
it 'finds by name' do
expect(results).to include(["files/images/wm.svg", nil])
@@ -70,6 +99,46 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
end
+ describe 'wiki search' do
+ let(:project) { create(:project, :public) }
+ let(:wiki) { build(:project_wiki, project: project) }
+ let!(:wiki_page) { wiki.create_page('Title', 'Content') }
+
+ subject(:results) { described_class.new(user, project, 'Content').objects('wiki_blobs') }
+
+ context 'when wiki is disabled' do
+ let(:project) { create(:project, :public, :wiki_disabled) }
+
+ it 'hides wiki blobs from members' do
+ project.add_reporter(user)
+
+ is_expected.to be_empty
+ end
+
+ it 'hides wiki blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when wiki is internal' do
+ let(:project) { create(:project, :public, :wiki_private) }
+
+ it 'finds wiki blobs for members' do
+ project.add_reporter(user)
+
+ is_expected.not_to be_empty
+ end
+
+ it 'hides wiki blobs from non-members' do
+ is_expected.to be_empty
+ end
+ end
+
+ it 'finds by content' do
+ expect(results).to include("master:Title.md:1:Content\n")
+ end
+ end
+
it 'does not list issues on private projects' do
issue = create(:issue, project: project)
@@ -79,7 +148,6 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
describe 'confidential issues' do
- let(:project) { create(:empty_project) }
let(:query) { 'issue' }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
@@ -277,6 +345,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
context 'by commit hash' do
let(:project) { create(:project, :public, :repository) }
let(:commit) { project.repository.commit('0b4bc9a') }
+
commit_hashes = { short: '0b4bc9a', full: '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
commit_hashes.each do |type, commit_hash|
diff --git a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
new file mode 100644
index 00000000000..0b8af5010ba
--- /dev/null
+++ b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170502101023_cleanup_namespaceless_pending_delete_projects.rb')
+
+describe CleanupNamespacelessPendingDeleteProjects do
+ before do
+ # Stub after_save callbacks that will fail when Project has no namespace
+ allow_any_instance_of(Project).to receive(:ensure_dir_exist).and_return(nil)
+ allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
+ end
+
+ describe '#up' do
+ it 'only cleans up pending delete projects' do
+ create(:empty_project)
+ create(:empty_project, pending_delete: true)
+ project = build(:empty_project, pending_delete: true, namespace_id: nil)
+ project.save(validate: false)
+
+ expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id.to_s]])
+
+ described_class.new.up
+ end
+
+ it 'does nothing when no pending delete projects without namespace found' do
+ create(:empty_project)
+ create(:empty_project, pending_delete: true)
+
+ expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async)
+
+ described_class.new.up
+ end
+ end
+end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index dd6514b3b50..cea8db7a926 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1634,7 +1634,7 @@ describe Repository, models: true do
context 'with an existing repository' do
it 'returns the README' do
- expect(repository.readme).to be_an_instance_of(Gitlab::Git::Blob)
+ expect(repository.readme).to be_an_instance_of(ReadmeBlob)
end
end
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 75b1fc7e216..1e5c96fe593 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -131,46 +131,6 @@ describe Snippet, models: true do
end
end
- describe '.accessible_to' do
- let(:author) { create(:author) }
- let(:project) { create(:empty_project) }
-
- let!(:public_snippet) { create(:snippet, :public) }
- let!(:internal_snippet) { create(:snippet, :internal) }
- let!(:private_snippet) { create(:snippet, :private, author: author) }
-
- let!(:project_public_snippet) { create(:snippet, :public, project: project) }
- let!(:project_internal_snippet) { create(:snippet, :internal, project: project) }
- let!(:project_private_snippet) { create(:snippet, :private, project: project) }
-
- it 'returns only public snippets when user is blank' do
- expect(described_class.accessible_to(nil)).to match_array [public_snippet, project_public_snippet]
- end
-
- it 'returns only public, and internal snippets for regular users' do
- user = create(:user)
-
- expect(described_class.accessible_to(user)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet]
- end
-
- it 'returns public, internal snippets and project private snippets for project members' do
- member = create(:user)
- project.team << [member, :developer]
-
- expect(described_class.accessible_to(member)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
- end
-
- it 'returns private snippets where the user is the author' do
- expect(described_class.accessible_to(author)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet]
- end
-
- it 'returns all snippets when for admins' do
- admin = create(:admin)
-
- expect(described_class.accessible_to(admin)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
- end
- end
-
describe '#participants' do
let(:project) { create(:empty_project, :public) }
let(:snippet) { create(:snippet, content: 'foo', project: project) }
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index d0758af57dd..e1771b636b8 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe ProjectSnippetPolicy, models: true do
- let(:current_user) { create(:user) }
+ let(:regular_user) { create(:user) }
+ let(:external_user) { create(:user, :external) }
+ let(:project) { create(:empty_project) }
let(:author_permissions) do
[
@@ -10,13 +12,15 @@ describe ProjectSnippetPolicy, models: true do
]
end
- subject { described_class.abilities(current_user, project_snippet).to_set }
+ def abilities(user, snippet_visibility)
+ snippet = create(:project_snippet, snippet_visibility, project: project)
- context 'public snippet' do
- let(:project_snippet) { create(:project_snippet, :public) }
+ described_class.abilities(user, snippet).to_set
+ end
+ context 'public snippet' do
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :public) }
it do
is_expected.to include(:read_project_snippet)
@@ -25,6 +29,17 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :public) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'external user' do
+ subject { abilities(external_user, :public) }
+
it do
is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -33,10 +48,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'internal snippet' do
- let(:project_snippet) { create(:project_snippet, :internal) }
-
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :internal) }
it do
is_expected.not_to include(:read_project_snippet)
@@ -45,6 +58,28 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :internal) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'external user' do
+ subject { abilities(external_user, :internal) }
+
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'project team member external user' do
+ subject { abilities(external_user, :internal) }
+
+ before { project.team << [external_user, :developer] }
+
it do
is_expected.to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -53,10 +88,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'private snippet' do
- let(:project_snippet) { create(:project_snippet, :private) }
-
context 'no user' do
- let(:current_user) { nil }
+ subject { abilities(nil, :private) }
it do
is_expected.not_to include(:read_project_snippet)
@@ -65,6 +98,8 @@ describe ProjectSnippetPolicy, models: true do
end
context 'regular user' do
+ subject { abilities(regular_user, :private) }
+
it do
is_expected.not_to include(:read_project_snippet)
is_expected.not_to include(*author_permissions)
@@ -72,7 +107,9 @@ describe ProjectSnippetPolicy, models: true do
end
context 'snippet author' do
- let(:project_snippet) { create(:project_snippet, :private, author: current_user) }
+ let(:snippet) { create(:project_snippet, :private, author: regular_user) }
+
+ subject { described_class.abilities(regular_user, snippet).to_set }
it do
is_expected.to include(:read_project_snippet)
@@ -80,8 +117,21 @@ describe ProjectSnippetPolicy, models: true do
end
end
- context 'project team member' do
- before { project_snippet.project.team << [current_user, :developer] }
+ context 'project team member normal user' do
+ subject { abilities(regular_user, :private) }
+
+ before { project.team << [regular_user, :developer] }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'project team member external user' do
+ subject { abilities(external_user, :private) }
+
+ before { project.team << [external_user, :developer] }
it do
is_expected.to include(:read_project_snippet)
@@ -90,7 +140,7 @@ describe ProjectSnippetPolicy, models: true do
end
context 'admin user' do
- let(:current_user) { create(:admin) }
+ subject { abilities(create(:admin), :private) }
it do
is_expected.to include(:read_project_snippet)
diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb
new file mode 100644
index 00000000000..9b293065797
--- /dev/null
+++ b/spec/views/projects/imports/new.html.haml_spec.rb
@@ -0,0 +1,22 @@
+require "spec_helper"
+
+describe "projects/imports/new.html.haml" do
+ let(:user) { create(:user) }
+
+ context 'when import fails' do
+ let(:project) { create(:project_empty_repo, import_status: :failed, import_error: '<a href="http://googl.com">Foo</a>', import_type: :gitlab_project, import_source: '/var/opt/gitlab/gitlab-rails/shared/tmp/project_exports/uploads/t.tar.gz', import_url: nil) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ it "escapes HTML in import errors" do
+ assign(:project, project)
+
+ render
+
+ expect(rendered).not_to have_link('Foo', href: "http://googl.com")
+ end
+ end
+end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 900f8d4732f..835a93e620e 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -31,7 +31,7 @@ describe 'projects/tree/show' do
it 'displays correctly' do
render
expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref)
- expect(rendered).to have_css('.readme-holder .file-content', text: ref)
+ expect(rendered).to have_css('.readme-holder')
end
end
end
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
new file mode 100644
index 00000000000..8533b7b85e9
--- /dev/null
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe NamespacelessProjectDestroyWorker do
+ subject { described_class.new }
+
+ before do
+ # Stub after_save callbacks that will fail when Project has no namespace
+ allow_any_instance_of(Project).to receive(:ensure_dir_exist).and_return(nil)
+ allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
+ end
+
+ describe '#perform' do
+ context 'project has namespace' do
+ it 'does not do anything' do
+ project = create(:empty_project)
+
+ subject.perform(project.id)
+
+ expect(Project.unscoped.all).to include(project)
+ end
+ end
+
+ context 'project has no namespace' do
+ let!(:project) do
+ project = build(:empty_project, namespace_id: nil)
+ project.save(validate: false)
+ project
+ end
+
+ context 'project not a fork of another project' do
+ it "truncates the project's team" do
+ expect_any_instance_of(ProjectTeam).to receive(:truncate)
+
+ subject.perform(project.id)
+ end
+
+ it 'deletes the project' do
+ subject.perform(project.id)
+
+ expect(Project.unscoped.all).not_to include(project)
+ end
+
+ it 'does not call unlink_fork' do
+ is_expected.not_to receive(:unlink_fork)
+
+ subject.perform(project.id)
+ end
+
+ it 'does not do anything in Project#remove_pages method' do
+ expect(Gitlab::PagesTransfer).not_to receive(:new)
+
+ subject.perform(project.id)
+ end
+ end
+
+ context 'project forked from another' do
+ let!(:parent_project) { create(:empty_project) }
+
+ before do
+ create(:forked_project_link, forked_to_project: project, forked_from_project: parent_project)
+ end
+
+ it 'closes open merge requests' do
+ merge_request = create(:merge_request, source_project: project, target_project: parent_project)
+
+ subject.perform(project.id)
+
+ expect(merge_request.reload).to be_closed
+ end
+
+ it 'destroys the link' do
+ subject.perform(project.id)
+
+ expect(parent_project.forked_project_links).to be_empty
+ end
+ end
+ end
+ end
+end