Commit ec3a50f1 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into 8998_skip_pending_commits_if_not_head

* upstream/master: (64 commits)
  Merge branch 'open-redirect-fix-continue-to' into 'security'
  Merge branch 'open-redirect-host-fix' into 'security'
  Merge branch 'path-disclosure-proj-import-export' into 'security'
  Merge branch '29364-private-projects-mr-fix'
  Merge branch '30125-markdown-security'
  Issue title realtime
  Update CHANGELOG.md for 8.16.9
  Update CHANGELOG.md for 8.17.5
  Update CHANGELOG.md for 9.0.4
  Add "search" optional param and docs for V4
  Use PDFLab to render PDFs in GitLab
  Separate Scala from Java in CI examples
  Fix broken link
  Reorganize CI examples, add more links
  Refactor CI index page
  Remove deprecated field from workhorse response
  Use gitlab-workhorse 1.4.3
  Document how ETag caching middleware handles query parameters
  Make group skip validation in the frontend
  Use NamespaceValidator::WILDCARD_ROUTES in ETag caching middleware
  ...
parents f7014cd5 aaa49c2c
......@@ -2,6 +2,31 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 9.0.4 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 9.0.3 (2017-04-05)
- Fix name colision when importing GitHub pull requests from forked repositories. !9719
- Fix GitHub Importer for PRs of deleted forked repositories. !9992
- Fix environment folder route when special chars present in environment name. !10250
- Improve Markdown rendering when a lot of merge requests are referenced. !10252
- Allow users to import GitHub projects to subgroups.
- Backport API changes needed to fix sticking in EE.
- Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery. (mhasbini)
- Make CI build to use optimistic locking only on status change.
- Fix race condition where a namespace would be deleted before a project was deleted.
- Fix linking to new issue with selected template via url parameter.
- Remove unnecessary ORDER BY clause when updating todos. (mhasbini)
- API: Make the /notes endpoint work with noteable iid instead of id.
- Fixes method not replacing URL parameters correctly and breaking pipelines pagination.
- Move issue, mr, todos next to profile dropdown in top nav.
## 9.0.2 (2017-03-29)
- Correctly update paths when changing a child group.
......@@ -303,6 +328,14 @@ entry.
- Change development tanuki favicon colors to match logo color order.
- API issues - support filtering by iids.
## 8.17.5 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 8.17.4 (2017-03-19)
- Only show public emails in atom feeds.
......@@ -516,6 +549,14 @@ entry.
- Remove deprecated GitlabCiService.
- Requeue pending deletion projects.
## 8.16.9 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 8.16.8 (2017-03-19)
- Only show public emails in atom feeds.
......
......@@ -476,10 +476,10 @@ AwardsHandler.prototype.setupSearch = function setupSearch() {
this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => {
const term = $(e.target).val().trim();
// Clean previous search results
$('ul.emoji-menu-search, h5.emoji-search').remove();
$('ul.emoji-menu-search, h5.emoji-search-title').remove();
if (term.length > 0) {
// Generate a search result block
const h5 = $('<h5 class="emoji-search" />').text('Search results');
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
const foundEmojis = this.searchEmojis(term).show();
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
......
/* eslint-disable no-new */
import Vue from 'vue';
import PDFLab from 'vendor/pdflab';
import workerSrc from 'vendor/pdf.worker';
Vue.use(PDFLab, {
workerSrc,
});
export default () => {
const el = document.getElementById('js-pdf-viewer');
new Vue({
el,
data() {
return {
error: false,
loadError: false,
loading: true,
pdf: el.dataset.endpoint,
};
},
methods: {
onLoad() {
this.loading = false;
},
onError(error) {
this.loading = false;
this.loadError = true;
this.error = error;
},
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="PDF loading">
</i>
</div>
<pdf-lab
v-if="!loadError"
:pdf="pdf"
@pdflabload="onLoad"
@pdflaberror="onError" />
<p
class="text-center"
v-if="error">
<span v-if="loadError">
An error occured whilst loading the file. Please try again later.
</span>
<span v-else>
An error occured whilst decoding the file.
</span>
</p>
</div>
`,
});
};
import renderPDF from './pdf';
document.addEventListener('DOMContentLoaded', renderPDF);
import JSZip from 'jszip';
import JSZipUtils from 'jszip-utils';
export default class SketchLoader {
constructor(container) {
this.container = container;
this.loadingIcon = this.container.querySelector('.js-loading-icon');
this.load();
}
load() {
return this.getZipFile()
.then(data => JSZip.loadAsync(data))
.then(asyncResult => asyncResult.files['previews/preview.png'].async('uint8array'))
.then((content) => {
const url = window.URL || window.webkitURL;
const blob = new Blob([new Uint8Array(content)], {
type: 'image/png',
});
const previewUrl = url.createObjectURL(blob);
this.render(previewUrl);
})
.catch(this.error.bind(this));
}
getZipFile() {
return new JSZip.external.Promise((resolve, reject) => {
JSZipUtils.getBinaryContent(this.container.dataset.endpoint, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
render(previewUrl) {
const previewLink = document.createElement('a');
const previewImage = document.createElement('img');
previewLink.href = previewUrl;
previewLink.target = '_blank';
previewImage.src = previewUrl;
previewImage.className = 'img-responsive';
previewLink.appendChild(previewImage);
this.container.appendChild(previewLink);
this.removeLoadingIcon();
}
error() {
const errorMsg = document.createElement('p');
errorMsg.className = 'prepend-top-default append-bottom-default text-center';
errorMsg.textContent = `
Cannot show preview. For previews on sketch files, they must have the file format
introduced by Sketch version 43 and above.
`;
this.container.appendChild(errorMsg);
this.removeLoadingIcon();
}
removeLoadingIcon() {
if (this.loadingIcon) {
this.loadingIcon.remove();
}
}
}
/* eslint-disable no-new */
import SketchLoader from './sketch';
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('js-sketch-viewer');
new SketchLoader(el);
});
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */
import Vue from 'vue';
import boardList from './board_list';
import boardBlankState from './board_blank_state';
require('./board_delete');
......@@ -16,7 +16,7 @@ require('./board_list');
gl.issueBoards.Board = Vue.extend({
template: '#js-board-template',
components: {
'board-list': gl.issueBoards.BoardList,
boardList,
'board-delete': gl.issueBoards.BoardDelete,
boardBlankState,
},
......
/* eslint-disable comma-dangle, space-before-function-paren, max-len */
/* global Sortable */
import Vue from 'vue';
import boardNewIssue from './board_new_issue';
import boardCard from './board_card';
import eventHub from '../eventhub';
(() => {
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
const Store = gl.issueBoards.BoardsStore;
gl.issueBoards.BoardList = Vue.extend({
template: '#js-board-list-template',
components: {
boardCard,
boardNewIssue,
export default {
name: 'BoardList',
props: {
disabled: {
type: Boolean,
required: true,
},
props: {
disabled: Boolean,
list: Object,
issues: Array,
loading: Boolean,
issueLinkBase: String,
rootPath: String,
list: {
type: Object,
required: true,
},
data () {
return {
scrollOffset: 250,
filters: Store.state.filters,
showCount: false,
showIssueForm: false
};
issues: {
type: Array,
required: true,
},
watch: {
filters: {
handler () {
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
},
deep: true
},
issues () {
this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
this.list.page += 1;
this.list.getIssues(false);
}
loading: {
type: Boolean,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
},
data() {
return {
scrollOffset: 250,
filters: Store.state.filters,
showCount: false,
showIssueForm: false,
};
},
components: {
boardCard,
boardNewIssue,
},
methods: {
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
loadNextPage() {
const getIssues = this.list.nextPage();
if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
}
if (getIssues) {
this.list.loadingMore = true;
getIssues.then(() => {
this.list.loadingMore = false;
});
}
},
methods: {
listHeight () {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight () {
return this.$refs.list.scrollHeight;
},
scrollTop () {
return this.$refs.list.scrollTop + this.listHeight();
toggleForm() {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
},
},
watch: {
filters: {
handler() {
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
},
loadNextPage () {
const getIssues = this.list.nextPage();
deep: true,
},
issues() {
this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() &&
this.list.issuesSize > this.list.issues.length) {
this.list.page += 1;
this.list.getIssues(false);
}
if (getIssues) {
this.list.loadingMore = true;
getIssues.then(() => {
this.list.loadingMore = false;
});
if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
}
},
toggleForm() {
this.showIssueForm = !this.showIssueForm;
},
},
created() {
gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
});
},
mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: document.querySelectorAll('.boards-list')[0],
group: 'issues',
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
dataIdAttr: 'data-issue-id',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
},
created() {
eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
},
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: document.querySelectorAll('.boards-list')[0],
group: 'issues',
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
dataIdAttr: 'data-issue-id',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
card.showDetail = false;
Store.moving.list = card.list;
Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId);
card.showDetail = false;
Store.moving.list = card.list;
Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId);
gl.issueBoards.onStart();
},
onAdd: (e) => {
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
gl.issueBoards.onStart();
},
onAdd: (e) => {
gl.issueBoards.BoardsStore
.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
this.$nextTick(() => {
e.item.remove();
});
},
onUpdate: (e) => {
const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
},
onMove(e) {
return !e.related.classList.contains('board-list-count');
}
});
this.$nextTick(() => {
e.item.remove();
});
},
onUpdate: (e) => {
const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
gl.issueBoards.BoardsStore
.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
},
onMove(e) {
return !e.related.classList.contains('board-list-count');
},
});
this.sortable = Sortable.create(this.$refs.list, options);
this.sortable = Sortable.create(this.$refs.list, options);
// Scroll event on list to load more
this.$refs.list.onscroll = () => {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
};
},
beforeDestroy() {
gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
},
});
})();
// Scroll event on list to load more
this.$refs.list.addEventListener('scroll', this.onScroll);
},
beforeDestroy() {
eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
this.$refs.list.removeEventListener('scroll', this.onScroll);
},
template: `
<div class="board-list-component">
<div
class="board-list-loading text-center"
aria-label="Loading issues"
v-if="loading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
</div>
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
class="board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
:class="{ 'is-smaller': showIssueForm }">
<board-card
v-for="(issue, index) in issues"
ref="issue"
:index="index"
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />
<li
class="board-list-count text-center"
v-if="showCount"
data-id="-1">
<i
class="fa fa-spinner fa-spin"
aria-label="Loading more issues"
aria-hidden="true"
v-show="list.loadingMore">
</i>
<span v-if="list.issues.length === list.issuesSize">
Showing all issues
</span>
<span v-else>
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
</span>
</li>
</ul>
</div>
`,
};
/* global ListIssue */
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
export default {
......@@ -49,7 +51,7 @@ export default {
},
cancel() {
this.title = '';
gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`);
eventHub.$emit(`hide-issue-form-${this.list.id}`);
},
},
mounted() {
......
......@@ -24,6 +24,7 @@ export default Vue.component('environment-component', {
state: store.state,
visibility: 'available',
isLoading: false,
isLoadingFolderContent: false,
cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment,
......@@ -68,15 +69,21 @@ export default Vue.component('environment-component', {
this.fetchEnvironments();
eventHub.$on('refreshEnvironments', this.fetchEnvironments);
eventHub.$on('toggleFolder', this.toggleFolder);
},
beforeDestroyed() {
eventHub.$off('refreshEnvironments');
eventHub.$off('toggleFolder');
},
methods: {
toggleRow(model) {
return this.store.toggleFolder(model.name);
toggleFolder(folder, folderUrl) {
this.store.toggleFolder(folder);
if (!folder.isOpen) {
this.fetchChildEnvironments(folder, folderUrl);
}
},
/**
......@@ -117,6 +124,21 @@ export default Vue.component('environment-component', {
new Flash('An error occurred while fetching the environments.');
});
},
fetchChildEnvironments(folder, folderUrl) {
this.isLoadingFolderContent = true;
this.service.getFolderContent(folderUrl)
.then(resp => resp.json())
.then((response) => {
this.store.setfolderContent(folder, response.environments);
this.isLoadingFolderContent = false;
})