/** * Copyright (C) 2018-2025 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ // utility functions import Cookies from 'js-cookie'; export function handleFetchError(response) { if (!response.ok) { throw response; } return response; } export function handleFetchErrors(responses) { for (let i = 0; i < responses.length; ++i) { if (!responses[i].ok) { throw responses[i]; } } return responses; } export function errorMessageFromResponse(errorData, defaultMessage) { let errorMessage = ''; try { const reason = JSON.parse(errorData['reason']); Object.entries(reason).forEach((keys, _) => { const key = keys[0]; const message = keys[1][0]; // take only the first issue errorMessage += `\n${key}: ${message}`; }); } catch (_) { errorMessage = errorData['reason']; // can't parse it, leave it raw } return errorMessage ? `Error: ${errorMessage}` : defaultMessage; } export function staticAsset(asset) { return `${__STATIC__}${asset}`; } export function csrfPost(url, headers = {}, body = null) { headers['X-CSRFToken'] = Cookies.get('csrftoken'); return fetch(url, { credentials: 'include', headers: headers, method: 'POST', body: body }); } export function isGitRepoUrl(url, pathPrefix = '/') { const allowedProtocols = ['http:', 'https:', 'git:']; if (allowedProtocols.find(protocol => protocol === url.protocol) === undefined) { return false; } if (!url.pathname.startsWith(pathPrefix)) { return false; } const re = new RegExp('[\\w\\.-]+\\/?(?!=.git)(?:\\.git\\/?)?$'); return re.test(url.pathname.slice(pathPrefix.length)); }; export function removeUrlFragment() { history.replaceState('', document.title, window.location.pathname + window.location.search); } export function selectText(startNode, endNode) { const selection = window.getSelection(); selection.removeAllRanges(); const range = document.createRange(); range.setStart(startNode, 0); if (endNode.nodeName !== '#text') { range.setEnd(endNode, endNode.childNodes.length); } else { range.setEnd(endNode, endNode.textContent.length); } selection.addRange(range); } export function textToHTML(text) { const textArea = document.createElement('textarea'); textArea.innerText = text; return textArea.innerHTML; } export function htmlAlert(type, message, closable = false) { let closeButton = ''; let extraClasses = ''; if (closable) { closeButton = `<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>`; extraClasses = 'alert-dismissible'; } return `<div class="alert alert-${type} ${extraClasses}" role="alert">${message}${closeButton}</div>`; } export function validateUrl(url, allowedProtocols = []) { let originUrl = null; let validUrl = true; try { originUrl = new URL(url); } catch (TypeError) { validUrl = false; } if (validUrl && allowedProtocols.length) { validUrl = ( allowedProtocols.find(protocol => protocol === originUrl.protocol) !== undefined ); } return validUrl ? originUrl : null; } export async function isArchivedOrigin(originPath, visitType) { if (!validateUrl(originPath)) { // Not a valid URL, return immediately return false; } else { let url = `${Urls.api_1_origin_visit_latest(originPath)}?require_snapshot=true`; if (visitType && visitType !== 'any') { url += `&visit_type=${visitType}`; } const response = await fetch(url); return response.ok; } } async function getCanonicalGithubOriginURL(ownerRepo) { const ghApiResponse = await fetch(`https://api.github.com/repos/${ownerRepo}`); if (ghApiResponse.ok && ghApiResponse.status === 200) { const ghApiResponseData = await ghApiResponse.json(); return ghApiResponseData.html_url; } } export async function getCanonicalOriginURL(originUrl) { let originUrlLower = originUrl.toLowerCase(); // github.com URL processing const ghUrlRegex = /^http[s]*:\/\/github.com\//; if (originUrlLower.match(ghUrlRegex)) { // remove trailing .git if (originUrlLower.endsWith('.git')) { originUrlLower = originUrlLower.slice(0, -4); } // remove trailing slash if (originUrlLower.endsWith('/')) { originUrlLower = originUrlLower.slice(0, -1); } // extract {owner}/{repo} const ownerRepo = originUrlLower.replace(ghUrlRegex, ''); // fetch canonical URL from github Web API const url = await getCanonicalGithubOriginURL(ownerRepo); if (url) { return url; } } const ghpagesUrlRegex = /^http[s]*:\/\/(?<owner>[^/]+).github.io\/(?<repo>[^/]+)\/?.*/; const parsedUrl = originUrlLower.match(ghpagesUrlRegex); if (parsedUrl) { const ownerRepo = `${parsedUrl.groups.owner}/${parsedUrl.groups.repo}`; // fetch canonical URL from github Web API const url = await getCanonicalGithubOriginURL(ownerRepo); if (url) { return url; } } return originUrl; } export function getHumanReadableDate(data) { // Display iso format date string into a human readable date // This is expected to be used by date field in datatable listing views // Example: 3/24/2022, 10:31:08 AM const date = new Date(data); return date.toLocaleString(); } export function genLink(sanitizedUrl, type, openInNewTab = false, linkText = '') { // Display link. It's up to the caller to sanitize sanitizedUrl first. if (type === 'display' && sanitizedUrl) { const encodedSanitizedUrl = encodeURI(sanitizedUrl); if (!linkText) { linkText = encodedSanitizedUrl; } let attrs = ''; if (openInNewTab) { attrs = 'target="_blank" rel="noopener noreferrer"'; } return `<a href="${encodedSanitizedUrl}" ${attrs}>${linkText}</a>`; } return sanitizedUrl; }