diff --git a/assets/src/bundles/webapp/webapp-utils.js b/assets/src/bundles/webapp/webapp-utils.js index 34e53093..db762941 100644 --- a/assets/src/bundles/webapp/webapp-utils.js +++ b/assets/src/bundles/webapp/webapp-utils.js @@ -1,397 +1,401 @@ /** * 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; let 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 let sidebarTransition = $('.main-sidebar, .main-sidebar:before').css('transition'); let 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 => { let 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) { let elts = document.elementsFromPoint(e.clientX, e.clientY); for (let 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) { let 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'); let searchQueryText = $('#swh-origins-search-top-input').val().trim(); let 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(() => { $('.swh-coverage-list').iFrameResize({heightCalculationMethod: 'taggedElement'}); fetch(Urls.stat_counters()) .then(response => response.json()) .then(data => { if (data.stat_counters && !$.isEmptyObject(data.stat_counters)) { for (let 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 (let 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) { $('#swh-web-modal-html .modal-title').text(title); $('#swh-web-modal-html .modal-body').html(html); $('#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 = '<select class="jump-to-page">'; for (let i = 1; i <= pageInfo.pages; ++i) { let selected = ''; if (i === pageInfo.page + 1) { selected = 'selected'; } content += `<option value="${i}" ${selected}>${i}</option>`; } content += `</select><span> / ${pageInfo.pages}</span>`; $(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 (let 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() { let 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/cypress/integration/admin.spec.js b/cypress/integration/admin.spec.js index 6e856ee1..bb0eddc1 100644 --- a/cypress/integration/admin.spec.js +++ b/cypress/integration/admin.spec.js @@ -1,205 +1,221 @@ /** * Copyright (C) 2019-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 */ const $ = Cypress.$; const defaultRedirect = '/admin/origin/save/'; let url; function logout() { cy.contains('a', 'logout') .click(); } describe('Test Admin Login/logout', function() { before(function() { url = this.Urls.admin(); }); it('should redirect to default page', function() { cy.visit(url) .get('input[name="username"]') .type('admin') .get('input[name="password"]') .type('admin') .get('.container form') .submit(); cy.location('pathname') .should('be.equal', defaultRedirect); logout(); }); it('should display admin-origin-save and deposit in sidebar', function() { cy.adminLogin(); cy.visit(url); cy.get(`.sidebar a[href="${this.Urls.admin_origin_save()}"]`) .should('be.visible'); cy.get(`.sidebar a[href="${this.Urls.admin_deposit()}"]`) .should('be.visible'); logout(); }); it('should display username on top-right', function() { cy.adminLogin(); cy.visit(url); cy.get('.swh-position-right') .should('contain', 'admin'); logout(); }); + it('should get info about a user logged in from javascript', function() { + cy.window().then(win => { + expect(win.swh.webapp.isUserLoggedIn()).to.be.false; + }); + cy.adminLogin(); + cy.visit(url); + cy.window().then(win => { + expect(win.swh.webapp.isUserLoggedIn()).to.be.true; + }); + logout(); + cy.visit(url); + cy.window().then(win => { + expect(win.swh.webapp.isUserLoggedIn()).to.be.false; + }); + }); + it('should prevent unauthorized access after logout', function() { cy.visit(this.Urls.admin_origin_save()) .location('pathname') .should('be.equal', '/admin/login/'); cy.visit(this.Urls.admin_deposit()) .location('pathname') .should('be.equal', '/admin/login/'); }); it('should redirect to correct page after login', function() { // mock calls to deposit list api to avoid possible errors // while running the test cy.intercept(`${this.Urls.admin_deposit_list()}**`, { body: { data: [], recordsTotal: 0, recordsFiltered: 0, draw: 1 } }); cy.visit(this.Urls.admin_deposit()) .location('search') .should('contain', `next=${this.Urls.admin_deposit()}`); cy.adminLogin(); cy.visit(this.Urls.admin_deposit()); cy.location('pathname') .should('be.equal', this.Urls.admin_deposit()); logout(); }); }); const existingRowToSelect = 'https://bitbucket.org/'; const originUrlListTestData = [ { listType: 'authorized', originToAdd: 'git://git.archlinux.org/', originToRemove: 'https://github.com/' }, { listType: 'unauthorized', originToAdd: 'https://random.org', originToRemove: 'https://gitlab.com' } ]; const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1); describe('Test Admin Origin Save Urls Filtering', function() { beforeEach(function() { cy.adminLogin(); cy.visit(this.Urls.admin_origin_save()); cy.contains('a', 'Origin urls filtering') .click() .wait(500); }); it(`should select or unselect a table row by clicking on it`, function() { cy.contains(`#swh-authorized-origin-urls tr`, existingRowToSelect) .click() .should('have.class', 'selected') .click() .should('not.have.class', 'selected'); }); originUrlListTestData.forEach(testData => { it(`should add a new origin url prefix in the ${testData.listType} list`, function() { const tabName = capitalize(testData.listType) + ' urls'; cy.contains('a', tabName) .click() .wait(500); cy.get(`#swh-${testData.listType}-origin-urls tr`).each(elt => { if ($(elt).text() === testData.originToAdd) { cy.get(elt).click(); cy.get(`#swh-remove-${testData.listType}-origin-url`).click(); } }); cy.get(`#swh-${testData.listType}-url-prefix`) .type(testData.originToAdd); cy.get(`#swh-add-${testData.listType}-origin-url`) .click(); cy.contains(`#swh-${testData.listType}-origin-urls tr`, testData.originToAdd) .should('be.visible'); cy.contains('.alert-success', `The origin url prefix has been successfully added in the ${testData.listType} list.`) .should('be.visible'); cy.get(`#swh-add-${testData.listType}-origin-url`) .click(); cy.contains('.alert-warning', `The provided origin url prefix is already registered in the ${testData.listType} list.`) .should('be.visible'); }); it(`should remove an origin url prefix from the ${testData.listType} list`, function() { const tabName = capitalize(testData.listType) + ' urls'; cy.contains('a', tabName) .click(); let originUrlMissing = true; cy.get(`#swh-${testData.listType}-origin-urls tr`).each(elt => { if ($(elt).text() === testData.originToRemove) { originUrlMissing = false; } }); if (originUrlMissing) { cy.get(`#swh-${testData.listType}-url-prefix`) .type(testData.originToRemove); cy.get(`#swh-add-${testData.listType}-origin-url`) .click(); cy.get('.alert-dismissible button').click(); } cy.contains(`#swh-${testData.listType}-origin-urls tr`, testData.originToRemove) .click(); cy.get(`#swh-remove-${testData.listType}-origin-url`).click(); cy.contains(`#swh-${testData.listType}-origin-urls tr`, testData.originToRemove) .should('not.exist'); }); }); }); diff --git a/swh/web/templates/layout.html b/swh/web/templates/layout.html index 7775b992..e97007a2 100644 --- a/swh/web/templates/layout.html +++ b/swh/web/templates/layout.html @@ -1,276 +1,278 @@ {% comment %} Copyright (C) 2015-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 {% endcomment %} <!DOCTYPE html> {% load js_reverse %} {% load static %} {% load render_bundle from webpack_loader %} {% load swh_templatetags %} <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>{% block title %}{% endblock %}</title> {% render_bundle 'vendors' %} {% render_bundle 'webapp' %} <script> /* @licstart The following is the entire license notice for the JavaScript code in this page. Copyright (C) 2015-2021 The Software Heritage developers This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. @licend The above is the entire license notice for the JavaScript code in this page. */ </script> <script> SWH_CONFIG = {{swh_client_config|jsonify}}; swh.webapp.sentryInit(SWH_CONFIG.sentry_dsn); </script> <script src="{% url 'js_reverse' %}" type="text/javascript"></script> <script> swh.webapp.setSwhObjectIcons({{ swh_object_icons|jsonify }}); </script> + {{ request.user.is_authenticated|json_script:"swh_user_logged_in" }} + {% block header %}{% endblock %} <link rel="icon" href="{% static 'img/icons/swh-logo-32x32.png' %}" sizes="32x32" /> <link rel="icon" href="{% static 'img/icons/swh-logo-archive-192x192.png' %}" sizes="192x192" /> <link rel="apple-touch-icon-precomposed" href="{% static 'img/icons/swh-logo-archive-180x180.png' %}" /> <link rel="search" type="application/opensearchdescription+xml" title="Software Heritage archive of public source code" href="{% static 'xml/swh-opensearch.xml' %}"> <meta name="msapplication-TileImage" content="{% static 'img/icons/swh-logo-archive-270x270.png' %}" /> {% if "production" in DJANGO_SETTINGS_MODULE %} <!-- Matomo --> <script type="text/javascript"> var _paq = window._paq = window._paq || []; _paq.push(['trackPageView']); (function() { var u="https://piwik.inria.fr/"; _paq.push(['setTrackerUrl', u+'matomo.php']); _paq.push(['setSiteId', '59']); var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s); })(); </script> <!-- End Matomo Code --> {% endif %} </head> <body class="hold-transition layout-fixed sidebar-mini"> <a id="top"></a> <div class="wrapper"> <div class="swh-top-bar"> <ul> <li class="swh-position-left"> <div id="swh-full-width-switch-container" class="custom-control custom-switch d-none d-lg-block d-xl-block"> <input type="checkbox" class="custom-control-input" id="swh-full-width-switch" onclick="swh.webapp.fullWidthToggled(event)"> <label class="custom-control-label font-weight-normal" for="swh-full-width-switch">Full width</label> </div> </li> <li> <a href="https://www.softwareheritage.org">Home</a> </li> <li> <a href="https://forge.softwareheritage.org/">Development</a> </li> <li> <a href="https://docs.softwareheritage.org/devel/">Documentation</a> </li> <li> <a class="swh-donate-link" href="https://www.softwareheritage.org/donate">Donate</a> </li> <li class="swh-position-right"> <a href="{{ status.server_url }}" target="_blank" class="swh-current-status mr-3 d-none d-lg-inline-block d-xl-inline-block"> <span id="swh-current-status-description">Operational</span> <i class="swh-current-status-indicator green"></i> </a> {% url 'logout' as logout_url %} {% if user.is_authenticated %} Logged in as {% if 'OIDC' in user.backend %} <a href="{% url 'oidc-profile' %}"><strong>{{ user.username }}</strong></a>, <a href="{% url 'oidc-logout' %}?next_path={% url 'logout' %}?remote_user=1">logout</a> {% else %} <strong>{{ user.username }}</strong>, <a href="{{ logout_url }}">logout</a> {% endif %} {% elif oidc_enabled %} {% if request.path != logout_url %} <a href="{% url 'oidc-login' %}?next_path={{ request.build_absolute_uri }}">login</a> {% else %} <a href="{% url 'oidc-login' %}">login</a> {% endif %} {% else %} {% if request.path != logout_url %} <a href="{% url 'login' %}?next={{ request.build_absolute_uri }}">login</a> {% else %} <a href="{% url 'login' %}">login</a> {% endif %} {% endif %} </li> </ul> </div> <nav class="main-header navbar navbar-expand-lg navbar-light navbar-static-top" id="swh-navbar"> <div class="navbar-header"> <a class="nav-link swh-push-menu" data-widget="pushmenu" data-enable-remember="true" href="#"> <i class="mdi mdi-24px mdi-menu mdi-fw" aria-hidden="true"></i> </a> </div> <div class="navbar" style="width: 94%;"> <div class="swh-navbar-content"> {% block navbar-content %}{% endblock %} {% if request.resolver_match.url_name != 'swh-web-homepage' and request.resolver_match.url_name != 'browse-search' %} <form class="form-horizontal d-none d-md-flex input-group swh-search-navbar needs-validation" id="swh-origins-search-top"> <input class="form-control" placeholder="Enter a SWHID to resolve or keyword(s) to search for in origin URLs" type="text" id="swh-origins-search-top-input" oninput="swh.webapp.validateSWHIDInput(this)" required/> <div class="input-group-append"> <button class="btn btn-primary" type="submit"> <i class="swh-search-icon mdi mdi-24px mdi-magnify" aria-hidden="true"></i> </button> </div> </form> {% endif %} </div> </div> </nav> </div> <aside class="swh-sidebar main-sidebar sidebar-no-expand sidebar-light-primary elevation-4"> <a href="{% url 'swh-web-homepage' %}" class="brand-link"> <img class="brand-image" src="{% static 'img/swh-logo.png' %}"> <div class="brand-text sitename" href="{% url 'swh-web-homepage' %}"> <span class="first-word">Software</span> <span class="second-word">Heritage</span> </div> </a> <a href="/" class="swh-words-logo"> <div class="swh-words-logo-swh"> <span class="first-word">Software</span> <span class="second-word">Heritage</span> </div> <span>Archive</span> </a> <div class="sidebar"> <nav class="mt-2"> <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false"> <li class="nav-header">Features</li> <li class="nav-item swh-search-item" title="Search archived software"> <a href="{% url 'browse-search' %}" class="nav-link swh-search-link"> <i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-magnify"></i> <p>Search</p> </a> </li> <li class="nav-item swh-vault-item" title="Download archived software from the Vault"> <a href="{% url 'browse-vault' %}" class="nav-link swh-vault-link"> <i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-download"></i> <p>Downloads</p> </a> </li> <li class="nav-item swh-origin-save-item" title="Request the saving of a software origin into the archive"> <a href="{% url 'origin-save' %}" class="nav-link swh-origin-save-link"> <i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-camera"></i> <p>Save code now</p> </a> </li> <li class="nav-item swh-help-item" title="How to browse the archive ?"> <a href="{% url 'browse-help' %}" class="nav-link swh-help-link"> <i style="color: #e20026;" class="nav-icon mdi mdi-24px mdi-help-circle"></i> <p>Help</p> </a> </li> {% if user.is_authenticated and user.is_staff %} <li class="nav-header">Administration</li> <li class="nav-item swh-origin-save-admin-item" title="Save code now administration"> <a href="{% url 'admin-origin-save' %}" class="nav-link swh-origin-save-admin-link"> <i style="color: #fecd1b;" class="nav-icon mdi mdi-24px mdi-camera"></i> <p>Save code now</p> </a> </li> <li class="nav-item swh-deposit-admin-item" title="Deposit administration"> <a href="{% url 'admin-deposit' %}" class="nav-link swh-deposit-admin-link"> <i style="color: #fecd1b;" class="nav-icon mdi mdi-24px mdi-folder-upload"></i> <p>Deposit</p> </a> </li> {% endif %} </ul> </nav> </div> </aside> <div class="content-wrapper"> <section class="content"> <div class="container" id="swh-web-content"> {% if swh_web_staging %} <div class="swh-corner-ribbon">Staging</div> {% endif %} {% block content %}{% endblock %} </div> </section> </div> {% include "includes/global-modals.html" %} <footer class="footer"> <div class="container text-center"> <a href="https://www.softwareheritage.org">Software Heritage</a> — Copyright (C) 2015–{% now "Y" %}, The Software Heritage developers. License: <a href="https://www.gnu.org/licenses/agpl.html">GNU AGPLv3+</a>. <br /> The source code of Software Heritage <em>itself</em> is available on our <a href="https://forge.softwareheritage.org/">development forge</a>. <br /> The source code files <em>archived</em> by Software Heritage are available under their own copyright and licenses. <br /> <span class="link-color">Terms of use: </span> <a href="https://www.softwareheritage.org/legal/bulk-access-terms-of-use/">Archive access</a>, <a href="https://www.softwareheritage.org/legal/api-terms-of-use/">API</a>- <a href="https://www.softwareheritage.org/contact/">Contact</a>- <a href="{% url 'jslicenses' %}" rel="jslicense">JavaScript license information</a>- <a href="{% url 'api-1-homepage' %}">Web API</a> </div> </footer> <div id="back-to-top"> <a href="#top"><img alt="back to top" src="{% static 'img/arrow-up-small.png' %}" /></a> </div> <script> swh.webapp.setContainerFullWidth(); var statusServerURL = {{ status.server_url|jsonify }}; var statusJsonPath = {{ status.json_path|jsonify }}; swh.webapp.initStatusWidget(statusServerURL + statusJsonPath); </script> </body> </html>