Page MenuHomeSoftware Heritage

No OneTemporary

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: `<img src="${swhSpinnerSrc}"></img>`
},
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 += `&nbsp;<a href="${sanitizedURL}" target="_blank" rel="noopener noreferrer">` +
- '<i class="mdi mdi-open-in-new" aria-hidden="true"></i></a>';
- 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 `<a href=${dashboardUrl}>${data}</a>`;
}
},
{
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, ';<br/>');
if (!linkText) {
linkText = formattedSWHID;
}
return `<a href="${browseUrl}">${linkText}</a>`;
}
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 `<a href="${sData}" ${attrs}>${linkText}</a>`;
- }
- 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,
'<i class="mdi mdi-open-in-new" aria-hidden="true"></i>');
}
return swhLink + '&nbsp;' + 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 `<button class="btn btn-default metadata">display</button>`;
}
}
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 `<div style="width: 200px; white-space: pre; overflow-x: auto;">${text}</div>`;
}
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(`<div id="swh-admin-deposit-list-exclude-wrapper">
<div id="swh-admin-deposit-list-exclude-div-wrapper" class="dataTables_filter">
<label>
Exclude:<input id="swh-admin-deposit-list-exclude-filter"
type="search"
value="check-deposit"
class="form-control form-control-sm"
placeholder="exclude-pattern" aria-controls="swh-admin-deposit-list">
</input>
</label>
</div>
</div>
`);
// 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 = $('<div/>').text(metadata).html();
swh.webapp.showModalHtml(`Metadata of deposit ${row.id}`,
`<pre style="max-height: 75vh;"><code class="xml">${escapedMetadata}</code></pre>`,
'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 =
`<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>`;
extraClasses = 'alert-dismissible';
}
return `<div class="alert alert-${type} ${extraClasses}" role="alert">${message}${closeButton}</div>`;
}
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]*:\/\/(?<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;
+}

File Metadata

Mime Type
text/x-diff
Expires
Jul 4 2025, 9:14 AM (6 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3246040

Event Timeline