diff --git a/assets/src/bundles/browse/swhid-utils.js b/assets/src/bundles/browse/swhid-utils.js index 36b5504d..d437b475 100644 --- a/assets/src/bundles/browse/swhid-utils.js +++ b/assets/src/bundles/browse/swhid-utils.js @@ -1,128 +1,128 @@ /** * Copyright (C) 2018-2021 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 ClipboardJS from 'clipboard'; import 'thirdparty/jquery.tabSlideOut/jquery.tabSlideOut'; import 'thirdparty/jquery.tabSlideOut/jquery.tabSlideOut.css'; import {BREAKPOINT_SM} from 'utils/constants'; export function swhIdObjectTypeToggled(event) { event.preventDefault(); $(event.target).tab('show'); } export function swhIdContextOptionToggled(event) { event.stopPropagation(); const swhIdElt = $(event.target).closest('.swhid-ui').find('.swhid'); const swhIdWithContext = $(event.target).data('swhid-with-context'); const swhIdWithContextUrl = $(event.target).data('swhid-with-context-url'); let currentSwhId = swhIdElt.text(); if ($(event.target).prop('checked')) { swhIdElt.attr('href', swhIdWithContextUrl); currentSwhId = swhIdWithContext.replace(/;/g, ';\n'); } else { const pos = currentSwhId.indexOf(';'); if (pos !== -1) { currentSwhId = currentSwhId.slice(0, pos); } swhIdElt.attr('href', '/' + currentSwhId); } swhIdElt.text(currentSwhId); addLinesInfo(); } function addLinesInfo() { const swhIdElt = $('#swhid-tab-content').find('.swhid'); let currentSwhId = swhIdElt.text().replace(/;\n/g, ';'); const lines = []; let linesPart = ';lines='; const linesRegexp = new RegExp(/L(\d+)/g); let line = linesRegexp.exec(window.location.hash); while (line) { lines.push(parseInt(line[1])); line = linesRegexp.exec(window.location.hash); } if (lines.length > 0) { linesPart += lines[0]; } if (lines.length > 1) { linesPart += '-' + lines[1]; } if ($('#swhid-context-option-content').prop('checked')) { currentSwhId = currentSwhId.replace(/;lines=\d+-*\d*/g, ''); if (lines.length > 0) { currentSwhId += linesPart; } swhIdElt.text(currentSwhId.replace(/;/g, ';\n')); swhIdElt.attr('href', '/' + currentSwhId); } } $(document).ready(() => { new ClipboardJS('.btn-swhid-copy', { text: trigger => { const swhId = $(trigger).closest('.swhid-ui').find('.swhid').text(); return swhId.replace(/;\n/g, ';'); } }); new ClipboardJS('.btn-swhid-url-copy', { text: trigger => { const swhIdUrl = $(trigger).closest('.swhid-ui').find('.swhid').attr('href'); return window.location.origin + swhIdUrl; } }); if (window.innerWidth * 0.7 > 1000) { $('#swh-identifiers').css('width', '1000px'); } // prevent automatic closing of SWHIDs tab during guided tour // as it is displayed programmatically function clickScreenToCloseFilter() { return $('.introjs-overlay').length > 0; } const tabSlideOptions = { tabLocation: 'right', clickScreenToCloseFilters: [clickScreenToCloseFilter, '.ui-slideouttab-panel', '.modal'], offset: function() { const width = $(window).width(); if (width < BREAKPOINT_SM) { return '250px'; } else { return '200px'; } } }; // ensure tab scrolling on small screens if (window.innerHeight < 600 || window.innerWidth < 500) { tabSlideOptions['otherOffset'] = '20px'; } // initiate the sliding identifiers tab $('#swh-identifiers').tabSlideOut(tabSlideOptions); // set the tab visible once the close animation is terminated - $('#swh-identifiers').css('display', 'block'); + $('#swh-identifiers').addClass('d-none d-sm-block'); $('.swhid-context-option').trigger('click'); // highlighted code lines changed $(window).on('hashchange', () => { addLinesInfo(); }); // highlighted code lines removed $('body').click(() => { addLinesInfo(); }); }); diff --git a/assets/src/bundles/webapp/code-highlighting.js b/assets/src/bundles/webapp/code-highlighting.js index fb7f03ac..f16f1247 100644 --- a/assets/src/bundles/webapp/code-highlighting.js +++ b/assets/src/bundles/webapp/code-highlighting.js @@ -1,117 +1,127 @@ /** - * Copyright (C) 2018-2019 The Software Heritage developers + * Copyright (C) 2018-2021 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 {removeUrlFragment} from 'utils/functions'; // keep track of the first highlighted line let firstHighlightedLine = null; // highlighting color const lineHighlightColor = 'rgb(193, 255, 193)'; // function to highlight a line export function highlightLine(i, firstHighlighted = false) { const lineTd = $(`.hljs-ln-line[data-line-number="${i}"]`); lineTd.css('background-color', lineHighlightColor); if (firstHighlighted) { firstHighlightedLine = i; } return lineTd; } +// function to highlight a range of lines +export function highlightLines(first, last) { + if (!first) { + return; + } + if (!last) { + last = first; + } + for (let i = first; i <= last; ++i) { + highlightLine(i); + } +} + // function to reset highlighting export function resetHighlightedLines() { firstHighlightedLine = null; $('.hljs-ln-line[data-line-number]').css('background-color', 'inherit'); } -export function scrollToLine(lineDomElt) { +export function scrollToLine(lineDomElt, offset = 70) { if ($(lineDomElt).closest('.swh-content').length > 0) { $('html, body').animate({ - scrollTop: $(lineDomElt).offset().top - 70 + scrollTop: $(lineDomElt).offset().top - offset }, 500); } } -export async function highlightCode(showLineNumbers = true, selector = 'code') { +export async function highlightCode(showLineNumbers = true, selector = 'code', + enableLinesSelection = true) { await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs'); // function to highlight lines based on a url fragment // in the form '#Lx' or '#Lx-Ly' function parseUrlFragmentForLinesToHighlight() { const lines = []; const linesRegexp = new RegExp(/L(\d+)/g); let line = linesRegexp.exec(window.location.hash); if (line === null) { return; } while (line) { lines.push(parseInt(line[1])); line = linesRegexp.exec(window.location.hash); } resetHighlightedLines(); if (lines.length === 1) { firstHighlightedLine = parseInt(lines[0]); scrollToLine(highlightLine(lines[0])); } else if (lines[0] < lines[lines.length - 1]) { firstHighlightedLine = parseInt(lines[0]); scrollToLine(highlightLine(lines[0])); - for (let i = lines[0] + 1; i <= lines[lines.length - 1]; ++i) { - highlightLine(i); - } + highlightLines(lines[0] + 1, lines[lines.length - 1]); } } $(document).ready(() => { // highlight code and add line numbers $(selector).each((i, elt) => { hljs.highlightElement(elt); if (showLineNumbers) { hljs.lineNumbersElement(elt, {singleLine: true}); } }); - if (!showLineNumbers) { + if (!showLineNumbers || !enableLinesSelection) { return; } // click handler to dynamically highlight line(s) // when the user clicks on a line number (lines range // can also be highlighted while holding the shift key) $('.swh-content').click(evt => { if (evt.target.classList.contains('hljs-ln-n')) { const line = parseInt($(evt.target).data('line-number')); if (evt.shiftKey && firstHighlightedLine && line > firstHighlightedLine) { const firstLine = firstHighlightedLine; resetHighlightedLines(); - for (let i = firstLine; i <= line; ++i) { - highlightLine(i); - } + highlightLines(firstLine, line); firstHighlightedLine = firstLine; window.location.hash = `#L${firstLine}-L${line}`; } else { resetHighlightedLines(); highlightLine(line); window.location.hash = `#L${line}`; scrollToLine(evt.target); } } else if ($(evt.target).closest('.hljs-ln').length) { resetHighlightedLines(); removeUrlFragment(); } }); // update lines highlighting when the url fragment changes $(window).on('hashchange', () => parseUrlFragmentForLinesToHighlight()); // schedule lines highlighting if any as hljs.lineNumbersElement() is async setTimeout(() => { parseUrlFragmentForLinesToHighlight(); }); }); } diff --git a/assets/src/bundles/webapp/iframes.js b/assets/src/bundles/webapp/iframes.js new file mode 100644 index 00000000..4a7b5301 --- /dev/null +++ b/assets/src/bundles/webapp/iframes.js @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2021 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 + */ + +export function showIframeInfoModal(objectType, objectSWHID) { + const html = ` +
+ You can embed that ${objectType} view in an external website + through the use of an iframe. Use the following HTML code + to do so. +
+<iframe style="width: 100%; height: 500px; border: 1px solid rgba(0, 0, 0, 0.125);"
+ src="${window.location.origin}${Urls.swhid_iframe(objectSWHID.replaceAll('\n', ''))}">
+</iframe>
+ `;
+ swh.webapp.showModalHtml(`Software Heritage ${objectType} iframe`, html, '1000px');
+ swh.webapp.highlightCode(false, '.swh-iframe-html');
+}
diff --git a/assets/src/bundles/webapp/index.js b/assets/src/bundles/webapp/index.js
index 1189dd91..d4f04bfd 100644
--- a/assets/src/bundles/webapp/index.js
+++ b/assets/src/bundles/webapp/index.js
@@ -1,28 +1,29 @@
/**
* Copyright (C) 2018-2021 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
*/
// webapp entrypoint bundle centralizing global custom stylesheets
// and utility js modules used in all swh-web applications
// global swh-web custom stylesheets
import './webapp.css';
import './breadcrumbs.css';
import './coverage.css';
export * from './webapp-utils';
// utility js modules
export * from './code-highlighting';
export * from './readme-rendering';
export * from './pdf-rendering';
export * from './notebook-rendering';
export * from './xss-filtering';
export * from './history-counters';
export * from './badges';
export * from './sentry';
export * from './math-typesetting';
export * from './status-widget';
+export * from './iframes';
diff --git a/assets/src/bundles/webapp/webapp-utils.js b/assets/src/bundles/webapp/webapp-utils.js
index 79a65270..5481088d 100644
--- a/assets/src/bundles/webapp/webapp-utils.js
+++ b/assets/src/bundles/webapp/webapp-utils.js
@@ -1,400 +1,402 @@
/**
* Copyright (C) 2018-2021 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 objectFitImages from 'object-fit-images';
import {selectText} from 'utils/functions';
import {BREAKPOINT_MD} from 'utils/constants';
let collapseSidebar = false;
const previousSidebarState = localStorage.getItem('remember.lte.pushmenu');
if (previousSidebarState !== undefined) {
collapseSidebar = previousSidebarState === 'sidebar-collapse';
}
$(document).on('DOMContentLoaded', () => {
// set state to collapsed on smaller devices
if ($(window).width() < BREAKPOINT_MD) {
collapseSidebar = true;
}
// restore previous sidebar state (collapsed/expanded)
if (collapseSidebar) {
// hack to avoid animated transition for collapsing sidebar
// when loading a page
const sidebarTransition = $('.main-sidebar, .main-sidebar:before').css('transition');
const sidebarEltsTransition = $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition');
$('.main-sidebar, .main-sidebar:before').css('transition', 'none');
$('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', 'none');
$('body').addClass('sidebar-collapse');
$('.swh-words-logo-swh').css('visibility', 'visible');
// restore transitions for user navigation
setTimeout(() => {
$('.main-sidebar, .main-sidebar:before').css('transition', sidebarTransition);
$('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', sidebarEltsTransition);
});
}
});
$(document).on('collapsed.lte.pushmenu', event => {
if ($('body').width() >= BREAKPOINT_MD) {
$('.swh-words-logo-swh').css('visibility', 'visible');
}
});
$(document).on('shown.lte.pushmenu', event => {
$('.swh-words-logo-swh').css('visibility', 'hidden');
});
function ensureNoFooterOverflow() {
$('body').css('padding-bottom', $('footer').outerHeight() + 'px');
}
$(document).ready(() => {
// redirect to last browse page if any when clicking on the 'Browse' entry
// in the sidebar
$(`.swh-browse-link`).click(event => {
const lastBrowsePage = sessionStorage.getItem('last-browse-page');
if (lastBrowsePage) {
event.preventDefault();
window.location = lastBrowsePage;
}
});
const mainSideBar = $('.main-sidebar');
function updateSidebarState() {
const body = $('body');
if (body.hasClass('sidebar-collapse') &&
!mainSideBar.hasClass('swh-sidebar-collapsed')) {
mainSideBar.removeClass('swh-sidebar-expanded');
mainSideBar.addClass('swh-sidebar-collapsed');
$('.swh-words-logo-swh').css('visibility', 'visible');
} else if (!body.hasClass('sidebar-collapse') &&
!mainSideBar.hasClass('swh-sidebar-expanded')) {
mainSideBar.removeClass('swh-sidebar-collapsed');
mainSideBar.addClass('swh-sidebar-expanded');
$('.swh-words-logo-swh').css('visibility', 'hidden');
}
// ensure correct sidebar state when loading a page
if (body.hasClass('hold-transition')) {
setTimeout(() => {
updateSidebarState();
});
}
}
// set sidebar state after collapse / expand animation
mainSideBar.on('transitionend', evt => {
updateSidebarState();
});
updateSidebarState();
// ensure footer do not overflow main content for mobile devices
// or after resizing the browser window
ensureNoFooterOverflow();
$(window).resize(function() {
ensureNoFooterOverflow();
if ($('body').hasClass('sidebar-collapse') && $('body').width() >= BREAKPOINT_MD) {
$('.swh-words-logo-swh').css('visibility', 'visible');
}
});
// activate css polyfill 'object-fit: contain' in old browsers
objectFitImages();
// reparent the modals to the top navigation div in order to be able
// to display them
$('.swh-browse-top-navigation').append($('.modal'));
let selectedCode = null;
function getCodeOrPreEltUnderPointer(e) {
const elts = document.elementsFromPoint(e.clientX, e.clientY);
for (const elt of elts) {
if (elt.nodeName === 'CODE' || elt.nodeName === 'PRE') {
return elt;
}
}
return null;
}
// click handler to set focus on code block for copy
$(document).click(e => {
selectedCode = getCodeOrPreEltUnderPointer(e);
});
function selectCode(event, selectedCode) {
if (selectedCode) {
const hljsLnCodeElts = $(selectedCode).find('.hljs-ln-code');
if (hljsLnCodeElts.length) {
selectText(hljsLnCodeElts[0], hljsLnCodeElts[hljsLnCodeElts.length - 1]);
} else {
selectText(selectedCode.firstChild, selectedCode.lastChild);
}
event.preventDefault();
}
}
// select the whole text of focused code block when user
// double clicks or hits Ctrl+A
$(document).dblclick(e => {
if ((e.ctrlKey || e.metaKey)) {
selectCode(e, getCodeOrPreEltUnderPointer(e));
}
});
$(document).keydown(e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
selectCode(e, selectedCode);
}
});
// show/hide back-to-top button
let scrollThreshold = 0;
scrollThreshold += $('.swh-top-bar').height() || 0;
scrollThreshold += $('.navbar').height() || 0;
$(window).scroll(() => {
if ($(window).scrollTop() > scrollThreshold) {
$('#back-to-top').css('display', 'block');
} else {
$('#back-to-top').css('display', 'none');
}
});
// navbar search form submission callback
$('#swh-origins-search-top').submit(event => {
event.preventDefault();
if (event.target.checkValidity()) {
$(event.target).removeClass('was-validated');
const searchQueryText = $('#swh-origins-search-top-input').val().trim();
const queryParameters = new URLSearchParams();
queryParameters.append('q', searchQueryText);
queryParameters.append('with_visit', true);
queryParameters.append('with_content', true);
window.location = `${Urls.browse_search()}?${queryParameters.toString()}`;
} else {
$(event.target).addClass('was-validated');
}
});
});
export function initPage(page) {
$(document).ready(() => {
// set relevant sidebar link to page active
$(`.swh-${page}-item`).addClass('active');
$(`.swh-${page}-link`).addClass('active');
// triggered when unloading the current page
$(window).on('unload', () => {
// backup current browse page
if (page === 'browse') {
sessionStorage.setItem('last-browse-page', window.location);
}
});
});
}
export function initHomePage() {
$(document).ready(async() => {
$('.swh-coverage-list').iFrameResize({heightCalculationMethod: 'taggedElement'});
const response = await fetch(Urls.stat_counters());
const data = await response.json();
if (data.stat_counters && !$.isEmptyObject(data.stat_counters)) {
for (const objectType of ['content', 'revision', 'origin', 'directory', 'person', 'release']) {
const count = data.stat_counters[objectType];
if (count !== undefined) {
$(`#swh-${objectType}-count`).html(count.toLocaleString());
} else {
$(`#swh-${objectType}-count`).closest('.swh-counter-container').hide();
}
}
} else {
$('.swh-counter').html('0');
}
if (data.stat_counters_history && !$.isEmptyObject(data.stat_counters_history)) {
for (const objectType of ['content', 'revision', 'origin']) {
const history = data.stat_counters_history[objectType];
if (history) {
swh.webapp.drawHistoryCounterGraph(`#swh-${objectType}-count-history`, history);
} else {
$(`#swh-${objectType}-count-history`).hide();
}
}
} else {
$('.swh-counter-history').hide();
}
});
initPage('home');
}
export function showModalMessage(title, message) {
$('#swh-web-modal-message .modal-title').text(title);
$('#swh-web-modal-message .modal-content p').text(message);
$('#swh-web-modal-message').modal('show');
}
export function showModalConfirm(title, message, callback) {
$('#swh-web-modal-confirm .modal-title').text(title);
$('#swh-web-modal-confirm .modal-content p').text(message);
$('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').bind('click', () => {
callback();
$('#swh-web-modal-confirm').modal('hide');
$('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').unbind('click');
});
$('#swh-web-modal-confirm').modal('show');
}
-export function showModalHtml(title, html) {
+export function showModalHtml(title, html, width = '500px') {
$('#swh-web-modal-html .modal-title').text(title);
$('#swh-web-modal-html .modal-body').html(html);
+ $('#swh-web-modal-html .modal-dialog').css('max-width', width);
+ $('#swh-web-modal-html .modal-dialog').css('width', width);
$('#swh-web-modal-html').modal('show');
}
export function addJumpToPagePopoverToDataTable(dataTableElt) {
dataTableElt.on('draw.dt', function() {
$('.paginate_button.disabled').css('cursor', 'pointer');
$('.paginate_button.disabled').on('click', event => {
const pageInfo = dataTableElt.page.info();
let content = ' / ${pageInfo.pages}`;
$(event.target).popover({
'title': 'Jump to page',
'content': content,
'html': true,
'placement': 'top',
'sanitizeFn': swh.webapp.filterXSS
});
$(event.target).popover('show');
$('.jump-to-page').on('change', function() {
$('.paginate_button.disabled').popover('hide');
const pageNumber = parseInt($(this).val()) - 1;
dataTableElt.page(pageNumber).draw('page');
});
});
});
dataTableElt.on('preXhr.dt', () => {
$('.paginate_button.disabled').popover('hide');
});
}
let swhObjectIcons;
export function setSwhObjectIcons(icons) {
swhObjectIcons = icons;
}
export function getSwhObjectIcon(swhObjectType) {
return swhObjectIcons[swhObjectType];
}
let browsedSwhObjectMetadata = {};
export function setBrowsedSwhObjectMetadata(metadata) {
browsedSwhObjectMetadata = metadata;
}
export function getBrowsedSwhObjectMetadata() {
return browsedSwhObjectMetadata;
}
// This will contain a mapping between an archived object type
// and its related SWHID metadata for each object reachable from
// the current browse view.
// SWHID metadata contain the following keys:
// * object_type: type of archived object
// * object_id: sha1 object identifier
// * swhid: SWHID without contextual info
// * swhid_url: URL to resolve SWHID without contextual info
// * context: object describing SWHID context
// * swhid_with_context: SWHID with contextual info
// * swhid_with_context_url: URL to resolve SWHID with contextual info
let swhidsContext_ = {};
export function setSwhIdsContext(swhidsContext) {
swhidsContext_ = {};
for (const swhidContext of swhidsContext) {
swhidsContext_[swhidContext.object_type] = swhidContext;
}
}
export function getSwhIdsContext() {
return swhidsContext_;
}
function setFullWidth(fullWidth) {
if (fullWidth) {
$('#swh-web-content').removeClass('container');
$('#swh-web-content').addClass('container-fluid');
} else {
$('#swh-web-content').removeClass('container-fluid');
$('#swh-web-content').addClass('container');
}
localStorage.setItem('swh-web-full-width', JSON.stringify(fullWidth));
$('#swh-full-width-switch').prop('checked', fullWidth);
}
export function fullWidthToggled(event) {
setFullWidth($(event.target).prop('checked'));
}
export function setContainerFullWidth() {
const previousFullWidthState = JSON.parse(localStorage.getItem('swh-web-full-width'));
if (previousFullWidthState !== null) {
setFullWidth(previousFullWidthState);
}
}
function coreSWHIDIsLowerCase(swhid) {
const qualifiersPos = swhid.indexOf(';');
let coreSWHID = swhid;
if (qualifiersPos !== -1) {
coreSWHID = swhid.slice(0, qualifiersPos);
}
return coreSWHID.toLowerCase() === coreSWHID;
}
export async function validateSWHIDInput(swhidInputElt) {
const swhidInput = swhidInputElt.value.trim();
let customValidity = '';
if (swhidInput.toLowerCase().startsWith('swh:')) {
if (coreSWHIDIsLowerCase(swhidInput)) {
const resolveSWHIDUrl = Urls.api_1_resolve_swhid(swhidInput);
const response = await fetch(resolveSWHIDUrl);
const responseData = await response.json();
if (responseData.hasOwnProperty('exception')) {
customValidity = responseData.reason;
}
} else {
const qualifiersPos = swhidInput.indexOf(';');
if (qualifiersPos === -1) {
customValidity = 'Invalid SWHID: all characters must be in lowercase. ';
customValidity += `Valid SWHID is ${swhidInput.toLowerCase()}`;
} else {
customValidity = 'Invalid SWHID: the core part must be in lowercase. ';
const coreSWHID = swhidInput.slice(0, qualifiersPos);
customValidity += `Valid SWHID is ${swhidInput.replace(coreSWHID, coreSWHID.toLowerCase())}`;
}
}
}
swhidInputElt.setCustomValidity(customValidity);
$(swhidInputElt).siblings('.invalid-feedback').text(customValidity);
}
export function isUserLoggedIn() {
return JSON.parse($('#swh_user_logged_in').text());
}
diff --git a/assets/src/bundles/webapp/webapp.css b/assets/src/bundles/webapp/webapp.css
index 7a200cd2..0b5f2a9e 100644
--- a/assets/src/bundles/webapp/webapp.css
+++ b/assets/src/bundles/webapp/webapp.css
@@ -1,749 +1,750 @@
/**
* Copyright (C) 2018-2021 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
*/
html {
height: 100%;
overflow-x: hidden;
scroll-behavior: auto !important;
}
body {
min-height: 100%;
margin: 0;
position: relative;
padding-bottom: 120px;
}
a:active,
a.active {
outline: none;
}
code {
background-color: #f9f2f4;
}
pre code {
background-color: transparent;
}
footer {
background-color: #262626;
color: #fff;
font-size: 0.8rem;
position: absolute;
bottom: 0;
width: 100%;
padding-top: 10px;
padding-bottom: 10px;
}
footer a,
footer a:visited,
footer a:hover {
color: #fecd1b;
}
footer a:hover {
text-decoration: underline;
}
.link-color {
color: #fecd1b;
}
pre {
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
padding: 9.5px;
font-size: 0.8rem;
}
.btn.active {
background-color: #e7e7e7;
}
.card {
margin-bottom: 5px !important;
overflow-x: auto;
}
.navbar-brand {
padding: 5px;
margin-right: 0;
}
.table {
margin-bottom: 0;
}
.swh-table thead {
background-color: #f2f4f5;
border-top: 1px solid rgba(0, 0, 0, 0.2);
font-weight: normal;
}
.swh-table-striped th {
border-top: none;
}
.swh-table-striped tbody tr:nth-child(even) {
background-color: #f2f4f5;
}
.swh-table-striped tbody tr:nth-child(odd) {
background-color: #fff;
}
.swh-web-app-link a {
text-decoration: none;
border: none;
}
.swh-web-app-link:hover {
background-color: #efeff2;
}
.table > thead > tr > th {
border-top: none;
border-bottom: 1px solid #e20026;
}
.table > tbody > tr > td {
border-style: none;
}
.sitename .first-word,
.sitename .second-word {
color: rgba(0, 0, 0, 0.75);
font-weight: normal;
font-size: 1.2rem;
}
.sitename .first-word {
font-family: "Alegreya Sans", sans-serif;
}
.sitename .second-word {
font-family: "Alegreya", serif;
}
.swh-counter {
font-size: 150%;
}
@media (max-width: 600px) {
.swh-counter-container {
margin-top: 1rem;
}
}
.swh-http-error {
margin: 0 auto;
text-align: center;
}
.swh-http-error-head {
color: #2d353c;
font-size: 30px;
}
.swh-http-error-code {
bottom: 60%;
color: #2d353c;
font-size: 96px;
line-height: 80px;
margin-bottom: 10px !important;
}
.swh-http-error-desc {
font-size: 12px;
color: #647788;
text-align: center;
}
.swh-http-error-desc pre {
display: inline-block;
text-align: left;
max-width: 800px;
white-space: pre-wrap;
}
.swh-list-unstyled {
list-style: none;
}
.popover {
max-width: 97%;
z-index: 40000;
}
.modal {
text-align: center;
padding: 0 !important;
z-index: 50000;
}
.modal::before {
content: "";
display: inline-block;
height: 100%;
vertical-align: middle;
margin-right: -4px;
}
.modal-dialog {
display: inline-block;
text-align: left;
vertical-align: middle;
}
.dropdown-submenu {
position: relative;
}
.dropdown-submenu .dropdown-menu {
top: 0;
left: -100%;
margin-top: -5px;
margin-left: -2px;
}
.dropdown-item:hover,
.dropdown-item:focus {
background-color: rgba(0, 0, 0, 0.1);
}
a.dropdown-left::before {
content: "\f035e";
font-family: "Material Design Icons";
display: block;
width: 20px;
height: 20px;
float: left;
margin-left: 0;
}
#swh-navbar {
border-top-style: none;
border-left-style: none;
border-right-style: none;
border-bottom-style: solid;
border-bottom-width: 5px;
border-image:
linear-gradient(
to right,
rgb(226, 0, 38) 0%,
rgb(254, 205, 27) 100%
)
1 1 1 1;
width: 100%;
padding: 5px;
margin-bottom: 10px;
margin-top: 30px;
justify-content: normal;
flex-wrap: nowrap;
height: 72px;
overflow: hidden;
}
#back-to-top {
display: none;
position: fixed;
bottom: 30px;
right: 30px;
z-index: 10;
}
#back-to-top a img {
display: block;
width: 32px;
height: 32px;
background-size: 32px 32px;
text-indent: -999px;
overflow: hidden;
}
.swh-top-bar {
direction: ltr;
height: 30px;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 99999;
background-color: #262626;
color: #fff;
text-align: center;
font-size: 14px;
}
.swh-top-bar ul {
margin-top: 4px;
padding-left: 0;
white-space: nowrap;
}
.swh-top-bar li {
display: inline-block;
margin-left: 10px;
margin-right: 10px;
}
.swh-top-bar a,
.swh-top-bar a:visited {
color: white;
}
.swh-top-bar a.swh-current-site,
.swh-top-bar a.swh-current-site:visited {
color: #fecd1b;
}
.swh-position-left {
position: absolute;
left: 0;
}
.swh-position-right {
position: absolute;
right: 0;
}
.swh-background-gray {
background: #efeff2;
}
.swh-donate-link {
border: 1px solid #fecd1b;
background-color: #e20026;
color: white !important;
padding: 3px;
border-radius: 3px;
}
.swh-navbar-content h4 {
padding-top: 7px;
}
.swh-navbar-content .bread-crumbs {
display: block;
margin-left: -40px;
}
.swh-navbar-content .bread-crumbs li.bc-no-root {
padding-top: 7px;
}
.main-sidebar {
margin-top: 30px;
}
.content-wrapper {
background: none;
}
.brand-image {
max-height: 40px;
}
.brand-link {
padding-top: 18.5px;
padding-bottom: 18px;
padding-left: 4px;
border-bottom: 5px solid #e20026 !important;
}
.navbar-header a,
ul.dropdown-menu a,
ul.navbar-nav a,
ul.nav-sidebar a {
border-bottom-style: none;
color: #323232;
}
.swh-sidebar .nav-link.active {
color: #323232 !important;
background-color: #e7e7e7 !important;
}
.nav-tabs .nav-link.active {
border-top: 3px solid #e20026;
}
.swh-image-error {
width: 80px;
height: auto;
}
@media (max-width: 600px) {
.card {
min-width: 80%;
}
.swh-image-error {
width: 40px;
height: auto;
}
.swh-donate-link {
display: none;
}
}
.form-check-label {
padding-top: 4px;
}
.swhid {
white-space: pre-wrap;
}
.swhid .swhid-option {
display: inline-block;
margin-right: 5px;
line-height: 1rem;
}
.nav-pills .nav-link:not(.active):hover {
color: rgba(0, 0, 0, 0.55);
}
.swh-heading-color {
color: #e20026 !important;
}
.sidebar-mini.sidebar-collapse .main-sidebar:hover {
width: 4.6rem;
}
.sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel > .info,
.sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-sidebar .nav-link p,
.sidebar-mini.sidebar-collapse .main-sidebar:hover .brand-text {
visibility: hidden !important;
}
.sidebar .nav-link p,
.main-sidebar .brand-text,
.sidebar .user-panel .info {
transition: none;
}
.sidebar-mini.sidebar-mini.sidebar-collapse .sidebar {
padding-right: 0;
}
.swh-words-logo {
position: absolute;
top: 0;
left: 0;
width: 73px;
height: 73px;
text-align: center;
font-size: 10pt;
color: rgba(0, 0, 0, 0.75);
}
.swh-words-logo:hover {
text-decoration: none;
}
.swh-words-logo-swh {
line-height: 1;
padding-top: 13px;
visibility: hidden;
}
hr.swh-faded-line {
border: 0;
height: 1px;
background-image: linear-gradient(to left, #f0f0f0, #8c8b8b, #f0f0f0);
}
/* Ensure that section title with link is colored like standard section title */
.swh-readme h1 a,
.swh-readme h2 a,
.swh-readme h3 a,
.swh-readme h4 a,
.swh-readme h5 a,
.swh-readme h6 a {
color: #e20026;
}
/* Make list compact in reStructuredText rendering */
.swh-rst li p {
margin-bottom: 0;
}
.swh-readme-txt pre {
background: none;
border: none;
}
.swh-coverage {
padding-top: 0.3rem;
border: none;
overflow: visible;
}
.swh-coverage a {
text-decoration: none;
}
.swh-coverage-col {
padding-left: 10px;
padding-right: 10px;
}
.swh-coverage-header {
padding-top: 0;
padding-bottom: 0;
}
.swh-coverage-logo {
display: block;
width: 100%;
height: 50px;
margin-left: auto;
margin-right: auto;
object-fit: contain;
/* polyfill for old browsers, see https://github.com/bfred-it/object-fit-images */
font-family: "object-fit: contain;";
}
.swh-coverage-list {
width: 100%;
height: 320px;
border: none;
}
.swh-coverage-chevron {
position: absolute;
right: 0;
}
.swh-coverage .card-header .mdi {
transition: 0.3s transform ease-in-out;
}
.swh-coverage .card-header .collapsed .mdi {
transform: rotate(90deg);
}
.swh-coverage-info-body {
max-height: 150px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin; /* Firefox only */
padding: 0;
}
/* Thin scrollbar for chromium based browsers */
.swh-coverage-info-body::-webkit-scrollbar {
width: 4px;
}
.swh-coverage-info-body::-webkit-scrollbar-track {
background: #eff0f1;
}
.swh-coverage-info-body::-webkit-scrollbar-thumb {
background: #909396;
}
tr.swh-tr-hover-highlight:hover td {
background: #ededed;
}
tr.swh-api-doc-route a {
text-decoration: none;
}
.swh-apidoc .col {
margin: 10px;
}
.swh-apidoc .swh-rst blockquote {
border: 0;
margin: 0;
padding: 0;
}
a.toggle-col {
text-decoration: none;
}
a.toggle-col.col-hidden {
text-decoration: line-through;
}
.admonition.warning {
background: #fcf8e3;
border: 1px solid #faebcc;
padding: 15px;
border-radius: 4px;
}
.admonition.warning p {
margin-bottom: 0;
}
.admonition.warning .first {
font-size: 1.5rem;
}
.swh-popover {
max-height: 50vh;
overflow-y: auto;
overflow-x: auto;
padding: 0;
}
@media screen and (min-width: 768px) {
.swh-popover {
max-width: 50vw;
}
}
.swh-popover pre {
white-space: pre-wrap;
margin-bottom: 0;
}
.d3-wrapper {
position: relative;
height: 0;
width: 100%;
padding: 0;
/* padding-bottom will be overwritten by JavaScript later */
padding-bottom: 100%;
}
.d3-wrapper > svg {
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
}
div.d3-tooltip {
position: absolute;
text-align: center;
width: auto;
height: auto;
padding: 2px;
font: 12px sans-serif;
background: white;
border: 1px solid black;
border-radius: 4px;
pointer-events: none;
}
.page-link {
cursor: pointer;
}
.wrapper {
overflow: hidden;
}
.swh-badge {
padding-bottom: 1rem;
cursor: pointer;
}
.swh-badge-html,
+.swh-iframe-html,
.swh-badge-md,
.swh-badge-rst {
white-space: pre-wrap !important;
}
/* Material Design icons alignment tweaks */
.mdi {
display: inline-block;
}
.mdi-camera {
transform: translateY(1px);
}
.mdi-source-commit {
transform: translateY(2px);
}
/* To set icons at a fixed width. Great to use when different
icon widths throw off alignment. Courtesy of Font Awesome. */
.mdi-fw {
text-align: center;
width: 1.25em;
}
.main-header .nav-link {
height: inherit;
}
.nav-sidebar .nav-header:not(:first-of-type) {
padding-top: 1rem;
}
.nav-sidebar .nav-link {
padding-top: 0;
padding-bottom: 0;
}
.nav-sidebar > .nav-item .nav-icon {
vertical-align: sub;
}
.swh-search-icon {
line-height: 1rem;
vertical-align: middle;
}
.swh-search-navbar {
position: absolute;
top: 0.7rem;
right: 15rem;
z-index: 50000;
width: 500px;
}
.sidebar-collapse .swh-search-navbar {
right: 4rem;
}
.swh-corner-ribbon {
width: 200px;
background: #fecd1b;
color: #e20026;
position: absolute;
text-align: center;
letter-spacing: 1px;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
top: 55px;
right: -50px;
left: auto;
transform: rotate(45deg);
z-index: 2000;
}
@media screen and (max-width: 600px) {
.swh-corner-ribbon {
top: 53px;
right: -65px;
}
}
.invalid-feedback {
font-size: 100%;
}
diff --git a/docs/index.rst b/docs/index.rst
index 147e8302..7d7cc33d 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,29 +1,30 @@
.. _swh-web:
Software Heritage - Web applications
====================================
Web application(s) to browse the archive, for both interactive (HTML UI)
and mechanized (REST API) use
.. toctree::
:maxdepth: 2
:caption: Contents:
developers-info
uri-scheme-api
uri-scheme-browse
uri-scheme-identifiers
+ uri-scheme-misc
Reference Documentation
-----------------------
.. toctree::
:maxdepth: 2
* :ref:`routingtable`
diff --git a/docs/uri-scheme-misc.rst b/docs/uri-scheme-misc.rst
new file mode 100644
index 00000000..3b0817b0
--- /dev/null
+++ b/docs/uri-scheme-misc.rst
@@ -0,0 +1,41 @@
+Miscellaneous URLs
+^^^^^^^^^^^^^^^^^^
+
+Iframe view for contents and directories
+----------------------------------------
+
+A subset of Software Heritage objects (contents and directories) can be embedded
+in external websites through the use of iframes. A dedicated endpoint serving
+a minimalist Web UI is available for that use case.
+
+.. http:get:: /embed/(swhid)/
+
+ Endpoint to embed Software Heritage objects in external websites using
+ an iframe.
+
+ :param string swhid: a SoftWare Heritage persistent IDentifier
+ object, or SWHID (see :ref:`persistent-identifiers` to learn more about its syntax)
+
+ :statuscode 200: no error
+ :statuscode 400: the provided identifier is malformed or
+ the object type is not supported by the view
+ :statuscode 404: requested object cannot be found in the archive
+
+ **Example:**
+
+ By adding HTML code similar to the one below in a web page,
+
+ .. code-block:: html
+
+
+
+ you will obtain the following rendering.
+
+ .. raw:: html
+
+
+
diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py
index 93a0df4f..17cf475b 100644
--- a/swh/web/common/utils.py
+++ b/swh/web/common/utils.py
@@ -1,390 +1,391 @@
# Copyright (C) 2017-2021 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
from datetime import datetime, timezone
import os
import re
from typing import Any, Dict, List, Optional
from bs4 import BeautifulSoup
from docutils.core import publish_parts
import docutils.parsers.rst
import docutils.utils
from docutils.writers.html5_polyglot import HTMLTranslator, Writer
from iso8601 import ParseError, parse_date
from pkg_resources import get_distribution
from prometheus_client.registry import CollectorRegistry
import requests
from requests.auth import HTTPBasicAuth
from django.core.cache import cache
from django.http import HttpRequest, QueryDict
from django.urls import reverse as django_reverse
from swh.web.common.exc import BadInputExc
from swh.web.common.typing import QueryParameters
from swh.web.config import ORIGIN_VISIT_TYPES, get_config
SWH_WEB_METRICS_REGISTRY = CollectorRegistry(auto_describe=True)
swh_object_icons = {
"alias": "mdi mdi-star",
"branch": "mdi mdi-source-branch",
"branches": "mdi mdi-source-branch",
"content": "mdi mdi-file-document",
"cnt": "mdi mdi-file-document",
"directory": "mdi mdi-folder",
"dir": "mdi mdi-folder",
"origin": "mdi mdi-source-repository",
"ori": "mdi mdi-source-repository",
"person": "mdi mdi-account",
"revisions history": "mdi mdi-history",
"release": "mdi mdi-tag",
"rel": "mdi mdi-tag",
"releases": "mdi mdi-tag",
"revision": "mdi mdi-rotate-90 mdi-source-commit",
"rev": "mdi mdi-rotate-90 mdi-source-commit",
"snapshot": "mdi mdi-camera",
"snp": "mdi mdi-camera",
"visits": "mdi mdi-calendar-month",
}
def reverse(
viewname: str,
url_args: Optional[Dict[str, Any]] = None,
query_params: Optional[QueryParameters] = None,
current_app: Optional[str] = None,
urlconf: Optional[str] = None,
request: Optional[HttpRequest] = None,
) -> str:
"""An override of django reverse function supporting query parameters.
Args:
viewname: the name of the django view from which to compute a url
url_args: dictionary of url arguments indexed by their names
query_params: dictionary of query parameters to append to the
reversed url
current_app: the name of the django app tighten to the view
urlconf: url configuration module
request: build an absolute URI if provided
Returns:
str: the url of the requested view with processed arguments and
query parameters
"""
if url_args:
url_args = {k: v for k, v in url_args.items() if v is not None}
url = django_reverse(
viewname, urlconf=urlconf, kwargs=url_args, current_app=current_app
)
if query_params:
query_params = {k: v for k, v in query_params.items() if v is not None}
if query_params and len(query_params) > 0:
query_dict = QueryDict("", mutable=True)
for k in sorted(query_params.keys()):
query_dict[k] = query_params[k]
url += "?" + query_dict.urlencode(safe="/;:")
if request is not None:
url = request.build_absolute_uri(url)
return url
def datetime_to_utc(date):
"""Returns datetime in UTC without timezone info
Args:
date (datetime.datetime): input datetime with timezone info
Returns:
datetime.datetime: datetime in UTC without timezone info
"""
if date.tzinfo and date.tzinfo != timezone.utc:
return date.astimezone(tz=timezone.utc)
else:
return date
def parse_iso8601_date_to_utc(iso_date: str) -> datetime:
"""Given an ISO 8601 datetime string, parse the result as UTC datetime.
Returns:
a timezone-aware datetime representing the parsed date
Raises:
swh.web.common.exc.BadInputExc: provided date does not respect ISO 8601 format
Samples:
- 2016-01-12
- 2016-01-12T09:19:12+0100
- 2007-01-14T20:34:22Z
"""
try:
date = parse_date(iso_date)
return datetime_to_utc(date)
except ParseError as e:
raise BadInputExc(e)
def shorten_path(path):
"""Shorten the given path: for each hash present, only return the first
8 characters followed by an ellipsis"""
sha256_re = r"([0-9a-f]{8})[0-9a-z]{56}"
sha1_re = r"([0-9a-f]{8})[0-9a-f]{32}"
ret = re.sub(sha256_re, r"\1...", path)
return re.sub(sha1_re, r"\1...", ret)
def format_utc_iso_date(iso_date, fmt="%d %B %Y, %H:%M UTC"):
"""Turns a string representation of an ISO 8601 datetime string
to UTC and format it into a more human readable one.
For instance, from the following input
string: '2017-05-04T13:27:13+02:00' the following one
is returned: '04 May 2017, 11:27 UTC'.
Custom format string may also be provided
as parameter
Args:
iso_date (str): a string representation of an ISO 8601 date
fmt (str): optional date formatting string
Returns:
str: a formatted string representation of the input iso date
"""
if not iso_date:
return iso_date
date = parse_iso8601_date_to_utc(iso_date)
return date.strftime(fmt)
def gen_path_info(path):
"""Function to generate path data navigation for use
with a breadcrumb in the swh web ui.
For instance, from a path /folder1/folder2/folder3,
it returns the following list::
[{'name': 'folder1', 'path': 'folder1'},
{'name': 'folder2', 'path': 'folder1/folder2'},
{'name': 'folder3', 'path': 'folder1/folder2/folder3'}]
Args:
path: a filesystem path
Returns:
list: a list of path data for navigation as illustrated above.
"""
path_info = []
if path:
sub_paths = path.strip("/").split("/")
path_from_root = ""
for p in sub_paths:
path_from_root += "/" + p
path_info.append({"name": p, "path": path_from_root.strip("/")})
return path_info
def parse_rst(text, report_level=2):
"""
Parse a reStructuredText string with docutils.
Args:
text (str): string with reStructuredText markups in it
report_level (int): level of docutils report messages to print
(1 info 2 warning 3 error 4 severe 5 none)
Returns:
docutils.nodes.document: a parsed docutils document
"""
parser = docutils.parsers.rst.Parser()
components = (docutils.parsers.rst.Parser,)
settings = docutils.frontend.OptionParser(
components=components
).get_default_values()
settings.report_level = report_level
document = docutils.utils.new_document("rst-doc", settings=settings)
parser.parse(text, document)
return document
def get_client_ip(request):
"""
Return the client IP address from an incoming HTTP request.
Args:
request (django.http.HttpRequest): the incoming HTTP request
Returns:
str: The client IP address
"""
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(",")[0]
else:
ip = request.META.get("REMOTE_ADDR")
return ip
browsers_supported_image_mimes = set(
[
"image/gif",
"image/png",
"image/jpeg",
"image/bmp",
"image/webp",
"image/svg",
"image/svg+xml",
]
)
def context_processor(request):
"""
Django context processor used to inject variables
in all swh-web templates.
"""
config = get_config()
if (
hasattr(request, "user")
and request.user.is_authenticated
and not hasattr(request.user, "backend")
):
# To avoid django.template.base.VariableDoesNotExist errors
# when rendering templates when standard Django user is logged in.
request.user.backend = "django.contrib.auth.backends.ModelBackend"
site_base_url = request.build_absolute_uri("/")
return {
"swh_object_icons": swh_object_icons,
"available_languages": None,
"swh_client_config": config["client_config"],
"oidc_enabled": bool(config["keycloak"]["server_url"]),
"browsers_supported_image_mimes": browsers_supported_image_mimes,
"keycloak": config["keycloak"],
"site_base_url": site_base_url,
"DJANGO_SETTINGS_MODULE": os.environ["DJANGO_SETTINGS_MODULE"],
"status": config["status"],
"swh_web_dev": "localhost" in site_base_url,
"swh_web_staging": any(
[
server_name in site_base_url
for server_name in config["staging_server_names"]
]
),
"swh_web_version": get_distribution("swh.web").version,
"visit_types": ORIGIN_VISIT_TYPES,
+ "iframe_mode": False,
}
def resolve_branch_alias(
snapshot: Dict[str, Any], branch: Optional[Dict[str, Any]]
) -> Optional[Dict[str, Any]]:
"""
Resolve branch alias in snapshot content.
Args:
snapshot: a full snapshot content
branch: a branch alias contained in the snapshot
Returns:
The real snapshot branch that got aliased.
"""
while branch and branch["target_type"] == "alias":
if branch["target"] in snapshot["branches"]:
branch = snapshot["branches"][branch["target"]]
else:
from swh.web.common import archive
snp = archive.lookup_snapshot(
snapshot["id"], branches_from=branch["target"], branches_count=1
)
if snp and branch["target"] in snp["branches"]:
branch = snp["branches"][branch["target"]]
else:
branch = None
return branch
class _NoHeaderHTMLTranslator(HTMLTranslator):
"""
Docutils translator subclass to customize the generation of HTML
from reST-formatted docstrings
"""
def __init__(self, document):
super().__init__(document)
self.body_prefix = []
self.body_suffix = []
_HTML_WRITER = Writer()
_HTML_WRITER.translator_class = _NoHeaderHTMLTranslator
def rst_to_html(rst: str) -> str:
"""
Convert reStructuredText document into HTML.
Args:
rst: A string containing a reStructuredText document
Returns:
Body content of the produced HTML conversion.
"""
settings = {
"initial_header_level": 2,
"halt_level": 4,
"traceback": True,
}
pp = publish_parts(rst, writer=_HTML_WRITER, settings_overrides=settings)
return f'{{ content }}
File | Mode | Size |
---|---|---|
{{ d.name }} | ||
{{ f.name }} | {{ f.perms }} | {{ f.length }} |
{{ error_description }}- + {% if not iframe_mode %} + + {% endif %}