summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/lib')
-rw-r--r--app/assets/javascripts/lib/dompurify.js53
-rw-r--r--app/assets/javascripts/lib/graphql.js1
-rw-r--r--app/assets/javascripts/lib/utils/axios_startup_calls.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js1
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/csrf.js8
-rw-r--r--app/assets/javascripts/lib/utils/css_utils.js19
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js49
-rw-r--r--app/assets/javascripts/lib/utils/experimentation.js3
-rw-r--r--app/assets/javascripts/lib/utils/highlight.js2
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js14
-rw-r--r--app/assets/javascripts/lib/utils/rails_ujs.js20
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js27
-rw-r--r--app/assets/javascripts/lib/utils/unit_format/index.js20
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js42
15 files changed, 218 insertions, 44 deletions
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
new file mode 100644
index 00000000000..d9ea57fbbce
--- /dev/null
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -0,0 +1,53 @@
+import { sanitize as dompurifySanitize, addHook } from 'dompurify';
+import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
+
+// Safely allow SVG <use> tags
+
+const defaultConfig = {
+ ADD_TAGS: ['use'],
+};
+
+// Only icons urls from `gon` are allowed
+const getAllowedIconUrls = (gon = window.gon) =>
+ [gon.sprite_file_icons, gon.sprite_icons].filter(Boolean);
+
+const isUrlAllowed = url => getAllowedIconUrls().some(allowedUrl => url.startsWith(allowedUrl));
+
+const isHrefSafe = url =>
+ isUrlAllowed(url) || isUrlAllowed(relativePathToAbsolute(url, getBaseURL()));
+
+const removeUnsafeHref = (node, attr) => {
+ if (!node.hasAttribute(attr)) {
+ return;
+ }
+
+ if (!isHrefSafe(node.getAttribute(attr))) {
+ node.removeAttribute(attr);
+ }
+};
+
+/**
+ * Sanitize icons' <use> tag attributes, to safely include
+ * svgs such as in:
+ *
+ * <svg viewBox="0 0 100 100">
+ * <use href="/assets/icons-xxx.svg#icon_name"></use>
+ * </svg>
+ *
+ * @param {Object} node - Node to sanitize
+ */
+const sanitizeSvgIcon = node => {
+ removeUnsafeHref(node, 'href');
+
+ // Note: `xlink:href` is deprecated, but still in use
+ // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
+ removeUnsafeHref(node, 'xlink:href');
+};
+
+addHook('afterSanitizeAttributes', node => {
+ if (node.tagName.toLowerCase() === 'use') {
+ sanitizeSvgIcon(node);
+ }
+});
+
+export const sanitize = (val, config = defaultConfig) => dompurifySanitize(val, config);
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index d2907f401c0..0e07f7d8e44 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -31,6 +31,7 @@ export default (resolvers = {}, config = {}) => {
// We set to `same-origin` which is default value in modern browsers.
// See https://github.com/whatwg/fetch/pull/585 for more information.
credentials: 'same-origin',
+ batchMax: config.batchMax || 10,
};
const uploadsLink = ApolloLink.split(
diff --git a/app/assets/javascripts/lib/utils/axios_startup_calls.js b/app/assets/javascripts/lib/utils/axios_startup_calls.js
index 7e2665b910c..7bb1da5aed5 100644
--- a/app/assets/javascripts/lib/utils/axios_startup_calls.js
+++ b/app/assets/javascripts/lib/utils/axios_startup_calls.js
@@ -7,7 +7,7 @@ const removeGitLabUrl = url => url.replace(gon.gitlab_url, '');
const getFullUrl = req => {
const url = removeGitLabUrl(req.url);
- return mergeUrlParams(req.params || {}, url);
+ return mergeUrlParams(req.params || {}, url, { sort: true });
};
const handleStartupCall = async ({ fetchCall }, req) => {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index bcf302cc262..fe1ac00fd1d 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -44,6 +44,7 @@ export const checkPageAndAction = (page, action) => {
return pagePath === page && actionPath === action;
};
+export const isInIncidentPage = () => checkPageAndAction('incidents', 'show');
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 993d51370ec..1a4ecc12f01 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -1,4 +1,5 @@
export const BYTES_IN_KIB = 1024;
+export const BYTES_IN_KB = 1000;
export const HIDDEN_CLASS = 'hidden';
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
diff --git a/app/assets/javascripts/lib/utils/csrf.js b/app/assets/javascripts/lib/utils/csrf.js
index ca9828c4682..3114a2a0dfb 100644
--- a/app/assets/javascripts/lib/utils/csrf.js
+++ b/app/assets/javascripts/lib/utils/csrf.js
@@ -1,5 +1,3 @@
-import $ from 'jquery';
-
/*
This module provides easy access to the CSRF token and caches
it for re-use. It also exposes some values commonly used in relation
@@ -20,7 +18,6 @@ If you need to compose a headers object, use the spread operator:
see also http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
and https://github.com/rails/jquery-rails/blob/v4.3.1/vendor/assets/javascripts/jquery_ujs.js#L59-L62
*/
-
const csrf = {
init() {
const tokenEl = document.querySelector('meta[name=csrf-token]');
@@ -52,9 +49,4 @@ const csrf = {
csrf.init();
-// use our cached token for any $.rails-generated AJAX requests
-if ($.rails) {
- $.rails.csrfToken = () => csrf.token;
-}
-
export default csrf;
diff --git a/app/assets/javascripts/lib/utils/css_utils.js b/app/assets/javascripts/lib/utils/css_utils.js
new file mode 100644
index 00000000000..90213221443
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/css_utils.js
@@ -0,0 +1,19 @@
+export function loadCSSFile(path) {
+ return new Promise(resolve => {
+ if (document.querySelector(`link[href="${path}"]`)) {
+ resolve();
+ } else {
+ const linkElement = document.createElement('link');
+ linkElement.type = 'text/css';
+ linkElement.rel = 'stylesheet';
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ linkElement.media = 'screen,print';
+ linkElement.onload = () => {
+ resolve();
+ };
+ linkElement.href = path;
+
+ document.head.appendChild(linkElement);
+ }
+ });
+}
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index b193a8b2c9a..6e78dc87c02 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -86,6 +86,21 @@ export const getDayName = date =>
][date.getDay()];
/**
+ * Returns the i18n month name from a given date
+ * @example
+ * formatDateAsMonth(new Date('2020-06-28')) -> 'Jun'
+ * @param {String} datetime where month is extracted from
+ * @param {Object} options
+ * @param {Boolean} options.abbreviated whether to use the abbreviated month string, or not
+ * @return {String} the i18n month name
+ */
+export function formatDateAsMonth(datetime, options = {}) {
+ const { abbreviated = true } = options;
+ const month = new Date(datetime).getMonth();
+ return getMonthNames(abbreviated)[month];
+}
+
+/**
* @example
* dateFormat('2017-12-05','mmm d, yyyy h:MMtt Z' ) -> "Dec 5, 2017 12:00am GMT+0000"
* @param {date} datetime
@@ -730,6 +745,21 @@ export const differenceInSeconds = (startDate, endDate) => {
};
/**
+ * A utility function which computes the difference in months
+ * between 2 dates.
+ *
+ * @param {Date} startDate the start date
+ * @param {Date} endDate the end date
+ *
+ * @return {Int} the difference in months
+ */
+export const differenceInMonths = (startDate, endDate) => {
+ const yearDiff = endDate.getYear() - startDate.getYear();
+ const monthDiff = endDate.getMonth() - startDate.getMonth();
+ return monthDiff + 12 * yearDiff;
+};
+
+/**
* A utility function which computes the difference in milliseconds
* between 2 dates.
*
@@ -743,3 +773,22 @@ export const differenceInMilliseconds = (startDate, endDate = Date.now()) => {
const endDateInMS = endDate instanceof Date ? endDate.getTime() : endDate;
return endDateInMS - startDateInMS;
};
+
+/**
+ * A utility which returns a new date at the first day of the month for any given date.
+ *
+ * @param {Date} date
+ *
+ * @return {Date} the date at the first day of the month
+ */
+export const dateAtFirstDayOfMonth = date => new Date(newDate(date).setDate(1));
+
+/**
+ * A utility function which checks if two dates match.
+ *
+ * @param {Date|Int} date1 Can be either a date object or a unix timestamp.
+ * @param {Date|Int} date2 Can be either a date object or a unix timestamp.
+ *
+ * @return {Boolean} true if the dates match
+ */
+export const datesMatch = (date1, date2) => differenceInMilliseconds(date1, date2) === 0;
diff --git a/app/assets/javascripts/lib/utils/experimentation.js b/app/assets/javascripts/lib/utils/experimentation.js
new file mode 100644
index 00000000000..555e76055e0
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/experimentation.js
@@ -0,0 +1,3 @@
+export function isExperimentEnabled(experimentKey) {
+ return Boolean(window.gon?.experiments?.[experimentKey]);
+}
diff --git a/app/assets/javascripts/lib/utils/highlight.js b/app/assets/javascripts/lib/utils/highlight.js
index 32553af9af3..8fa8af670b3 100644
--- a/app/assets/javascripts/lib/utils/highlight.js
+++ b/app/assets/javascripts/lib/utils/highlight.js
@@ -1,5 +1,5 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { sanitize } from 'dompurify';
+import { sanitize } from '~/lib/dompurify';
/**
* Wraps substring matches with HTML `<span>` elements.
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index bc87232f40b..2424d6cbf3b 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -1,4 +1,4 @@
-import { BYTES_IN_KIB } from './constants';
+import { BYTES_IN_KIB, BYTES_IN_KB } from './constants';
import { sprintf, __ } from '~/locale';
/**
@@ -35,6 +35,18 @@ export function formatRelevantDigits(number) {
}
/**
+ * Utility function that calculates KB of the given bytes.
+ * Note: This method calculates KiloBytes as opposed to
+ * Kibibytes. For Kibibytes, bytesToKiB should be used.
+ *
+ * @param {Number} number bytes
+ * @return {Number} KiB
+ */
+export function bytesToKB(number) {
+ return number / BYTES_IN_KB;
+}
+
+/**
* Utility function that calculates KiB of the given bytes.
*
* @param {Number} number bytes
diff --git a/app/assets/javascripts/lib/utils/rails_ujs.js b/app/assets/javascripts/lib/utils/rails_ujs.js
new file mode 100644
index 00000000000..8b40cc7bd11
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/rails_ujs.js
@@ -0,0 +1,20 @@
+import Rails from '@rails/ujs';
+
+export const initRails = () => {
+ // eslint-disable-next-line no-underscore-dangle
+ if (!window._rails_loaded) {
+ Rails.start();
+
+ // Count XHR requests for tests. See spec/support/helpers/wait_for_requests.rb
+ window.pendingRailsUJSRequests = 0;
+ document.body.addEventListener('ajax:complete', () => {
+ window.pendingRailsUJSRequests -= 1;
+ });
+
+ document.body.addEventListener('ajax:beforeSend', () => {
+ window.pendingRailsUJSRequests += 1;
+ });
+ }
+};
+
+export { Rails };
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index f4c6e4e3584..dfb86787788 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -42,9 +42,7 @@ function convertMonacoSelectionToAceFormat(sel) {
}
function getEditorSelectionRange(editor) {
- return window.gon.features?.monacoBlobs
- ? convertMonacoSelectionToAceFormat(editor.getSelection())
- : editor.getSelectionRange();
+ return convertMonacoSelectionToAceFormat(editor.getSelection());
}
function editorBlockTagText(text, blockTag, selected, editor) {
@@ -56,9 +54,6 @@ function editorBlockTagText(text, blockTag, selected, editor) {
if (shouldRemoveBlock) {
if (blockTag !== null) {
- // ace is globally defined
- // eslint-disable-next-line no-undef
- const { Range } = ace.require('ace/range');
const lastLine = lines[selectionRange.end.row + 1];
const rangeWithBlockTags = new Range(
lines[selectionRange.start.row - 1],
@@ -110,12 +105,7 @@ function moveCursor({
const endPosition = startPosition + select.length;
return textArea.setSelectionRange(startPosition, endPosition);
} else if (editor) {
- if (window.gon.features?.monacoBlobs) {
- editor.selectWithinSelection(select, tag);
- } else {
- editor.navigateLeft(tag.length - tag.indexOf(select));
- editor.getSelection().selectAWord();
- }
+ editor.selectWithinSelection(select, tag);
return;
}
}
@@ -139,11 +129,7 @@ function moveCursor({
}
} else if (editor && editorSelectionStart.row === editorSelectionEnd.row) {
if (positionBetweenTags) {
- if (window.gon.features?.monacoBlobs) {
- editor.moveCursor(tag.length * -1);
- } else {
- editor.navigateLeft(tag.length);
- }
+ editor.moveCursor(tag.length * -1);
}
}
}
@@ -166,6 +152,7 @@ export function insertMarkdownText({
let editorSelectionEnd;
let lastNewLine;
let textToInsert;
+ selected = selected.toString();
if (editor) {
const selectionRange = getEditorSelectionRange(editor);
@@ -265,11 +252,7 @@ export function insertMarkdownText({
}
if (editor) {
- if (window.gon.features?.monacoBlobs) {
- editor.replaceSelectedText(textToInsert, select);
- } else {
- editor.insert(textToInsert);
- }
+ editor.replaceSelectedText(textToInsert, select);
} else {
insertText(textArea, textToInsert);
}
diff --git a/app/assets/javascripts/lib/utils/unit_format/index.js b/app/assets/javascripts/lib/utils/unit_format/index.js
index adf374db66c..9f979f7ea4b 100644
--- a/app/assets/javascripts/lib/utils/unit_format/index.js
+++ b/app/assets/javascripts/lib/utils/unit_format/index.js
@@ -61,8 +61,8 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => {
* @function
* @param {Number} value - Number to format
* @param {Number} fractionDigits - precision decimals
- * @param {Number} maxLength - Max lenght of formatted number
- * if lenght is exceeded, exponential format is used.
+ * @param {Number} maxLength - Max length of formatted number
+ * if length is exceeded, exponential format is used.
*/
return numberFormatter();
}
@@ -73,8 +73,8 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => {
* @function
* @param {Number} value - Number to format, `1` is rendered as `100%`
* @param {Number} fractionDigits - number of precision decimals
- * @param {Number} maxLength - Max lenght of formatted number
- * if lenght is exceeded, exponential format is used.
+ * @param {Number} maxLength - Max length of formatted number
+ * if length is exceeded, exponential format is used.
*/
return numberFormatter('percent');
}
@@ -85,8 +85,8 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => {
* @function
* @param {Number} value - Number to format, `100` is rendered as `100%`
* @param {Number} fractionDigits - number of precision decimals
- * @param {Number} maxLength - Max lenght of formatted number
- * if lenght is exceeded, exponential format is used.
+ * @param {Number} maxLength - Max length of formatted number
+ * if length is exceeded, exponential format is used.
*/
return numberFormatter('percent', 1 / 100);
}
@@ -100,8 +100,8 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => {
* @function
* @param {Number} value - Number to format, `1` is rendered as `1s`
* @param {Number} fractionDigits - number of precision decimals
- * @param {Number} maxLength - Max lenght of formatted number
- * if lenght is exceeded, exponential format is used.
+ * @param {Number} maxLength - Max length of formatted number
+ * if length is exceeded, exponential format is used.
*/
return suffixFormatter(s__('Units|s'));
}
@@ -112,8 +112,8 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => {
* @function
* @param {Number} value - Number to format, `1` is formatted as `1ms`
* @param {Number} fractionDigits - number of precision decimals
- * @param {Number} maxLength - Max lenght of formatted number
- * if lenght is exceeded, exponential format is used.
+ * @param {Number} maxLength - Max length of formatted number
+ * if length is exceeded, exponential format is used.
*/
return suffixFormatter(s__('Units|ms'));
}
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index e9c3fe0a406..a9f6901de32 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -16,7 +16,7 @@ function decodeUrlParameter(val) {
return decodeURIComponent(val.replace(/\+/g, '%20'));
}
-function cleanLeadingSeparator(path) {
+export function cleanLeadingSeparator(path) {
return path.replace(PATH_SEPARATOR_LEADING_REGEX, '');
}
@@ -73,6 +73,7 @@ export function getParameterValues(sParam, url = window.location) {
* @param {String} url
* @param {Object} options
* @param {Boolean} options.spreadArrays - split array values into separate key/value-pairs
+ * @param {Boolean} options.sort - alphabetically sort params in the returned url (in asc order, i.e., a-z)
*/
export function mergeUrlParams(params, url, options = {}) {
const { spreadArrays = false, sort = false } = options;
@@ -255,6 +256,15 @@ export function getBaseURL() {
}
/**
+ * Takes a URL and returns content from the start until the final '/'
+ *
+ * @param {String} url - full url, including protocol and host
+ */
+export function stripFinalUrlSegment(url) {
+ return new URL('.', url).href;
+}
+
+/**
* Returns true if url is an absolute URL
*
* @param {String} url
@@ -282,6 +292,15 @@ export function isBase64DataUrl(url) {
}
/**
+ * Returns true if url is a blob: type url
+ *
+ * @param {String} url
+ */
+export function isBlobUrl(url) {
+ return /^blob:/.test(url);
+}
+
+/**
* Returns true if url is an absolute or root-relative URL
*
* @param {String} url
@@ -434,3 +453,24 @@ export function getHTTPProtocol(url) {
const protocol = url.split(':');
return protocol.length > 1 ? protocol[0] : undefined;
}
+
+/**
+ * Strips the filename from the given path by removing every non-slash character from the end of the
+ * passed parameter.
+ * @param {string} path
+ */
+export function stripPathTail(path = '') {
+ return path.replace(/[^/]+$/, '');
+}
+
+export function getURLOrigin(url) {
+ if (!url) {
+ return window.location.origin;
+ }
+
+ try {
+ return new URL(url).origin;
+ } catch (e) {
+ return null;
+ }
+}