diff options
Diffstat (limited to 'app/assets/javascripts/lib')
-rw-r--r-- | app/assets/javascripts/lib/dompurify.js | 53 | ||||
-rw-r--r-- | app/assets/javascripts/lib/graphql.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/axios_startup_calls.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/common_utils.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/constants.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/csrf.js | 8 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/css_utils.js | 19 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/datetime_utility.js | 49 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/experimentation.js | 3 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/highlight.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/number_utils.js | 14 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/rails_ujs.js | 20 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/text_markdown.js | 27 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/unit_format/index.js | 20 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/url_utility.js | 42 |
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; + } +} |