diff --git a/assets/src/bundles/add_forge/create-request.js b/assets/src/bundles/add_forge/create-request.js index 75ff23c6..31b64f47 100644 --- a/assets/src/bundles/add_forge/create-request.js +++ b/assets/src/bundles/add_forge/create-request.js @@ -1,131 +1,124 @@ /** * Copyright (C) 2022 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 */ import {handleFetchError, errorMessageFromResponse, csrfPost, - getHumanReadableDate} from 'utils/functions'; + getHumanReadableDate, genLink} from 'utils/functions'; import userRequestsFilterCheckboxFn from 'utils/requests-filter-checkbox.ejs'; import {swhSpinnerSrc} from 'utils/constants'; let requestBrowseTable; const addForgeCheckboxId = 'swh-add-forge-user-filter'; const userRequestsFilterCheckbox = userRequestsFilterCheckboxFn({ 'inputId': addForgeCheckboxId, 'checked': true // by default, display only user requests }); export function onCreateRequestPageLoad() { $(document).ready(() => { $('#requestCreateForm').submit(async function(event) { event.preventDefault(); try { const response = await csrfPost($(this).attr('action'), {'Content-Type': 'application/x-www-form-urlencoded'}, $(this).serialize()); handleFetchError(response); $('#userMessageDetail').empty(); $('#userMessage').text('Your request has been submitted'); $('#userMessage').removeClass('badge-danger'); $('#userMessage').addClass('badge-success'); requestBrowseTable.draw(); // redraw the table to update the list } catch (errorResponse) { $('#userMessageDetail').empty(); let errorMessage; const errorData = await errorResponse.json(); // if (errorResponse.content_type === 'text/plain') { // does not work? if (errorResponse.status === 409) { errorMessage = errorData; } else { // assuming json response // const exception = errorData['exception']; errorMessage = errorMessageFromResponse( errorData, 'An unknown error occurred during the request creation'); } $('#userMessage').text(errorMessage); $('#userMessage').removeClass('badge-success'); $('#userMessage').addClass('badge-danger'); } }); populateRequestBrowseList(); // Load existing requests }); } export function populateRequestBrowseList() { requestBrowseTable = $('#add-forge-request-browse') .on('error.dt', (e, settings, techNote, message) => { $('#add-forge-browse-request-error').text(message); }) .DataTable({ serverSide: true, processing: true, language: { processing: `` }, retrieve: true, searching: true, info: false, // Layout configuration, see [1] for more details // [1] https://datatables.net/reference/option/dom dom: '<"row"<"col-sm-3"l><"col-sm-6 text-left user-requests-filter"><"col-sm-3"f>>' + '<"row"<"col-sm-12"tr>>' + '<"row"<"col-sm-5"i><"col-sm-7"p>>', ajax: { 'url': Urls.add_forge_request_list_datatables(), data: (d) => { const checked = $(`#${addForgeCheckboxId}`).prop('checked'); // If this function is called while the page is loading, 'checked' is // undefined. As the checkbox defaults to being checked, coerce this to true. if (swh.webapp.isUserLoggedIn() && (checked === undefined || checked)) { d.user_requests_only = '1'; } } }, fnInitComplete: function() { if (swh.webapp.isUserLoggedIn()) { $('div.user-requests-filter').html(userRequestsFilterCheckbox); $(`#${addForgeCheckboxId}`).on('change', () => { requestBrowseTable.draw(); }); } }, columns: [ { data: 'submission_date', name: 'submission_date', render: getHumanReadableDate }, { data: 'forge_type', name: 'forge_type', render: $.fn.dataTable.render.text() }, { data: 'forge_url', name: 'forge_url', - render: function(data, type, row) { - if (type === 'display') { - let html = ''; - const sanitizedURL = $.fn.dataTable.render.text().display(data); - html += sanitizedURL; - html += ` ` + - ''; - return html; - } - return data; + render: (data, type, row) => { + const sanitizedURL = $.fn.dataTable.render.text().display(data); + return genLink(sanitizedURL, type, true); } }, { data: 'status', name: 'status', render: function(data, type, row, meta) { return swh.add_forge.formatRequestStatusName(data); } } ] }); } diff --git a/assets/src/bundles/add_forge/moderation-dashboard.js b/assets/src/bundles/add_forge/moderation-dashboard.js index 2035ebbe..fd062281 100644 --- a/assets/src/bundles/add_forge/moderation-dashboard.js +++ b/assets/src/bundles/add_forge/moderation-dashboard.js @@ -1,72 +1,75 @@ /** * Copyright (C) 2022 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 */ -import {getHumanReadableDate} from 'utils/functions'; +import {getHumanReadableDate, genLink} from 'utils/functions'; export function onModerationPageLoad() { populateModerationList(); } export async function populateModerationList() { $('#swh-add-forge-now-moderation-list') .on('error.dt', (e, settings, techNote, message) => { $('#swh-add-forge-now-moderation-list-error').text(message); }) .DataTable({ serverSide: true, processing: true, searching: true, info: false, dom: '<<"d-flex justify-content-between align-items-center"f' + '<"#list-exclude">l>rt<"bottom"ip>>', ajax: { 'url': Urls.add_forge_request_list_datatables() }, columns: [ { data: 'id', name: 'id', render: function(data, type, row, meta) { const dashboardUrl = Urls.add_forge_now_request_dashboard(data); return `${data}`; } }, { data: 'submission_date', name: 'submission_date', render: getHumanReadableDate }, { data: 'forge_type', name: 'forge_type', render: $.fn.dataTable.render.text() }, { data: 'forge_url', name: 'forge_url', - render: $.fn.dataTable.render.text() + render: (data, type, row) => { + const sanitizedURL = $.fn.dataTable.render.text().display(data); + return genLink(sanitizedURL, type, true); + } }, { data: 'last_moderator', name: 'last_moderator', render: $.fn.dataTable.render.text() }, { data: 'last_modified_date', name: 'last_modified_date', render: getHumanReadableDate }, { data: 'status', name: 'status', render: function(data, type, row, meta) { return swh.add_forge.formatRequestStatusName(data); } } ] }); } diff --git a/assets/src/bundles/admin/deposit.js b/assets/src/bundles/admin/deposit.js index efb28896..ef8f7dec 100644 --- a/assets/src/bundles/admin/deposit.js +++ b/assets/src/bundles/admin/deposit.js @@ -1,203 +1,188 @@ /** * Copyright (C) 2018-2022 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 */ -import {getHumanReadableDate} from 'utils/functions'; +import {getHumanReadableDate, genLink} from 'utils/functions'; function genSwhLink(data, type, linkText = '') { if (type === 'display' && data && data.startsWith('swh')) { const browseUrl = Urls.browse_swhid(data); const formattedSWHID = data.replace(/;/g, ';
'); if (!linkText) { linkText = formattedSWHID; } return `${linkText}`; } return data; } -function genLink(data, type, openInNewTab = false, linkText = '') { - if (type === 'display' && data) { - const sData = encodeURI(data); - if (!linkText) { - linkText = sData; - } - let attrs = ''; - if (openInNewTab) { - attrs = 'target="_blank" rel="noopener noreferrer"'; - } - return `${linkText}`; - } - return data; -} - export function initDepositAdmin(username, isStaff) { let depositsTable; $(document).ready(() => { $.fn.dataTable.ext.errMode = 'none'; depositsTable = $('#swh-admin-deposit-list') .on('error.dt', (e, settings, techNote, message) => { $('#swh-admin-deposit-list-error').text(message); }) .DataTable({ serverSide: true, processing: true, // let's define the order of table options display // f: (f)ilter // l: (l)ength changing // r: p(r)ocessing // t: (t)able // i: (i)nfo // p: (p)agination // see https://datatables.net/examples/basic_init/dom.html dom: '<<"d-flex justify-content-between align-items-center"f' + '<"#list-exclude">l>rt<"bottom"ip>>', // div#list-exclude is a custom filter added next to dataTable // initialization below through js dom manipulation, see // https://datatables.net/examples/advanced_init/dom_toolbar.html ajax: { url: Urls.admin_deposit_list(), data: d => { d.excludePattern = $('#swh-admin-deposit-list-exclude-filter').val(); } }, columns: [ { data: 'id', name: 'id' }, { data: 'type', name: 'type' }, { data: 'uri', name: 'uri', render: (data, type, row) => { const sanitizedURL = $.fn.dataTable.render.text().display(data); let swhLink = ''; let originLink = ''; if (row.swhid_context && data) { swhLink = genSwhLink(row.swhid_context, type, sanitizedURL); } else if (data) { swhLink = sanitizedURL; } if (data) { originLink = genLink(sanitizedURL, type, true, ''); } return swhLink + ' ' + originLink; } }, { data: 'reception_date', name: 'reception_date', render: getHumanReadableDate }, { data: 'status', name: 'status' }, { data: 'raw_metadata', name: 'raw_metadata', render: (data, type, row) => { if (type === 'display') { if (row.raw_metadata) { return ``; } } return data; } }, { data: 'status_detail', name: 'status_detail', render: (data, type, row) => { if (type === 'display' && data) { let text = data; if (typeof data === 'object') { text = JSON.stringify(data, null, 4); } return `
${text}
`; } return data; }, orderable: false, visible: false }, { data: 'swhid', name: 'swhid', render: (data, type, row) => { return genSwhLink(data, type); }, orderable: false, visible: false }, { data: 'swhid_context', name: 'swhid_context', render: (data, type, row) => { return genSwhLink(data, type); }, orderable: false, visible: false } ], scrollX: true, scrollY: '50vh', scrollCollapse: true, order: [[0, 'desc']] }); // Some more customization is needed on the table $('div#list-exclude').html(`
`); // Show a modal when the "metadata" button is clicked $('#swh-admin-deposit-list tbody').on('click', 'tr button.metadata', function() { var row = depositsTable.row(this.parentNode.parentNode).data(); var metadata = row.raw_metadata; var escapedMetadata = $('
').text(metadata).html(); swh.webapp.showModalHtml(`Metadata of deposit ${row.id}`, `
${escapedMetadata}
`, '90%'); swh.webapp.highlightCode(); }); // Adding exclusion pattern update behavior, when typing, update search $('#swh-admin-deposit-list-exclude-filter').keyup(function() { depositsTable.draw(); }); // at last draw the table depositsTable.draw(); }); $('a.toggle-col').on('click', function(e) { e.preventDefault(); var column = depositsTable.column($(this).attr('data-column')); column.visible(!column.visible()); if (column.visible()) { $(this).removeClass('col-hidden'); } else { $(this).addClass('col-hidden'); } }); } diff --git a/assets/src/utils/functions.js b/assets/src/utils/functions.js index 2c92a5f6..90960f1d 100644 --- a/assets/src/utils/functions.js +++ b/assets/src/utils/functions.js @@ -1,168 +1,184 @@ /** - * Copyright (C) 2018-2020 The Software Heritage developers + * Copyright (C) 2018-2022 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 htmlAlert(type, message, closable = false) { let closeButton = ''; let extraClasses = ''; if (closable) { closeButton = ``; extraClasses = 'alert-dismissible'; } return ``; } export function isValidURL(string) { try { new URL(string); } catch (_) { return false; } return true; } export async function isArchivedOrigin(originPath) { if (!isValidURL(originPath)) { // Not a valid URL, return immediately return false; } else { const response = await fetch(Urls.api_1_origin(originPath)); return response.ok && response.status === 200; // Success response represents an archived origin } } 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]*:\/\/(?[^/]+).github.io\/(?[^/]+)\/?.*/; 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 `${linkText}`; + } + return sanitizedUrl; +}