diff --git a/cypress/e2e/origin-save.cy.js b/cypress/e2e/origin-save.cy.js index 7e232f6d..42663326 100644 --- a/cypress/e2e/origin-save.cy.js +++ b/cypress/e2e/origin-save.cy.js @@ -1,882 +1,914 @@ /** * Copyright (C) 2019-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 */ let url; const origin = { type: 'git', url: 'https://git.example.org/user/repo' }; const $ = Cypress.$; const saveCodeMsg = { 'success': 'The "save code now" request has been accepted and will be processed as soon as possible.', 'warning': 'The "save code now" request has been put in pending state and may be accepted for processing after manual review.', 'rejected': 'The "save code now" request has been rejected because the provided origin url is blacklisted.', 'rateLimit': 'The rate limit for "save code now" requests has been reached. Please try again later.', 'not-found': 'The provided url does not exist', 'unknownError': 'An unexpected error happened when submitting the "save code now request', 'csrfError': 'CSRF Failed: Referrer checking failed - no Referrer.' }; const anonymousVisitTypes = ['bzr', 'cvs', 'git', 'hg', 'svn']; const allVisitTypes = ['archives', 'bzr', 'cvs', 'git', 'hg', 'svn']; function makeOriginSaveRequest(originType, originUrl) { cy.get('#swh-input-origin-url') .type(originUrl) .get('#swh-input-visit-type') .select(originType) .get('#swh-save-origin-form button[type=submit]') .click(); } function checkAlertVisible(alertType, msg) { cy.get('#swh-origin-save-request-status') .should('be.visible') .find(`.alert-${alertType}`) .should('be.visible') .and('contain', msg); } // Stub requests to save an origin function stubSaveRequest({ requestUrl, visitType = 'git', saveRequestStatus, originUrl, saveTaskStatus, responseStatus = 200, // For error code with the error message in the 'reason' key response errorMessage = '', saveRequestDate = new Date(), visitDate = new Date(), - visitStatus = null + visitStatus = null, + fromWebhook = false } = {}) { let response; if (responseStatus !== 200 && errorMessage) { response = { 'reason': errorMessage }; } else { response = genOriginSaveResponse({visitType: visitType, saveRequestStatus: saveRequestStatus, originUrl: originUrl, saveRequestDate: saveRequestDate, saveTaskStatus: saveTaskStatus, visitDate: visitDate, - visitStatus: visitStatus + visitStatus: visitStatus, + fromWebhook: fromWebhook }); } cy.intercept('POST', requestUrl, {body: response, statusCode: responseStatus}) .as('saveRequest'); } // Mocks API response : /save/(:visit_type)/(:origin_url) // visit_type : {'git', 'hg', 'svn', ...} function genOriginSaveResponse({ visitType = 'git', saveRequestStatus, originUrl, saveRequestDate = new Date(), saveTaskStatus, visitDate = new Date(), - visitStatus + visitStatus, + fromWebhook = false } = {}) { return { 'visit_type': visitType, 'save_request_status': saveRequestStatus, 'origin_url': originUrl, 'id': 1, 'save_request_date': saveRequestDate ? saveRequestDate.toISOString() : null, 'save_task_status': saveTaskStatus, 'visit_date': visitDate ? visitDate.toISOString() : null, - 'visit_status': visitStatus + 'visit_status': visitStatus, + 'from_webhook': fromWebhook }; }; function loadSaveRequestsListPage() { // click on tab to visit requests list page cy.get('#swh-origin-save-requests-list-tab').click(); // two XHR requests are sent by datatables when initializing requests table cy.wait(['@saveRequestsList', '@saveRequestsList']); // ensure datatable got rendered cy.wait(100); } describe('Origin Save Tests', function() { before(function() { url = this.Urls.origin_save(); this.originSaveUrl = this.Urls.api_1_save_origin(origin.type, origin.url); }); beforeEach(function() { cy.fixture('origin-save').as('originSaveJSON'); cy.fixture('save-task-info').as('saveTaskInfoJSON'); cy.visit(url); }); it('should format appropriately values depending on their type', function() { const inputValues = [ // null values stay null {type: 'json', value: null, expectedValue: null}, {type: 'date', value: null, expectedValue: null}, {type: 'raw', value: null, expectedValue: null}, {type: 'duration', value: null, expectedValue: null}, // non null values formatted depending on their type {type: 'json', value: '{}', expectedValue: '"{}"'}, {type: 'date', value: '04/04/2021 01:00:00', expectedValue: '4/4/2021, 1:00:00 AM'}, {type: 'raw', value: 'value-for-identity', expectedValue: 'value-for-identity'}, {type: 'duration', value: '10', expectedValue: '10 seconds'}, {type: 'duration', value: 100, expectedValue: '100 seconds'} ]; cy.window().then(win => { inputValues.forEach(function(input, index, array) { const actualValue = win.swh.save_code_now.formatValuePerType(input.type, input.value); assert.equal(actualValue, input.expectedValue); }); }); }); it('should display accepted message when accepted', function() { stubSaveRequest({requestUrl: this.originSaveUrl, saveRequestStatus: 'accepted', originUrl: origin.url, saveTaskStatus: 'not yet scheduled'}); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); it('should validate gitlab subproject url', function() { const gitlabSubProjectUrl = 'https://gitlab.com/user/project/sub/'; const originSaveUrl = this.Urls.api_1_save_origin('git', gitlabSubProjectUrl); stubSaveRequest({requestUrl: originSaveUrl, saveRequestStatus: 'accepted', originurl: gitlabSubProjectUrl, saveTaskStatus: 'not yet scheduled'}); makeOriginSaveRequest('git', gitlabSubProjectUrl); cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); it('should validate project url with _ in username', function() { const gitlabSubProjectUrl = 'https://gitlab.com/user_name/project.git'; const originSaveUrl = this.Urls.api_1_save_origin('git', gitlabSubProjectUrl); stubSaveRequest({requestUrl: originSaveUrl, saveRequestStatus: 'accepted', originurl: gitlabSubProjectUrl, saveTaskStatus: 'not yet scheduled'}); makeOriginSaveRequest('git', gitlabSubProjectUrl); cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); it('should validate git repo url starting with https://git.code.sf.net/u/', function() { const sfUserGirProjectUrl = 'https://git.code.sf.net/u/username/project.git'; const originSaveUrl = this.Urls.api_1_save_origin('git', sfUserGirProjectUrl); stubSaveRequest({requestUrl: originSaveUrl, saveRequestStatus: 'accepted', originurl: sfUserGirProjectUrl, saveTaskStatus: 'not yet scheduled'}); makeOriginSaveRequest('git', sfUserGirProjectUrl); cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); it('should display warning message when pending', function() { stubSaveRequest({requestUrl: this.originSaveUrl, saveRequestStatus: 'pending', originUrl: origin.url, saveTaskStatus: 'not created'}); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('warning', saveCodeMsg['warning']); }); }); it('should show error when the origin does not exist (status: 400)', function() { stubSaveRequest({requestUrl: this.originSaveUrl, originUrl: origin.url, responseStatus: 400, errorMessage: saveCodeMsg['not-found']}); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('danger', saveCodeMsg['not-found']); }); }); it('should show error when csrf validation failed (status: 403)', function() { stubSaveRequest({requestUrl: this.originSaveUrl, saveRequestStatus: 'rejected', originUrl: origin.url, saveTaskStatus: 'not created', responseStatus: 403, errorMessage: saveCodeMsg['csrfError']}); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('danger', saveCodeMsg['csrfError']); }); }); it('should show error when origin is rejected (status: 403)', function() { stubSaveRequest({requestUrl: this.originSaveUrl, saveRequestStatus: 'rejected', originUrl: origin.url, saveTaskStatus: 'not created', responseStatus: 403, errorMessage: saveCodeMsg['rejected']}); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('danger', saveCodeMsg['rejected']); }); }); it('should show error when rate limited (status: 429)', function() { stubSaveRequest({requestUrl: this.originSaveUrl, saveRequestStatus: 'Request was throttled. Expected available in 60 seconds.', originUrl: origin.url, saveTaskStatus: 'not created', responseStatus: 429}); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('danger', saveCodeMsg['rateLimit']); }); }); it('should show error when unknown error occurs (status other than 200, 403, 429)', function() { stubSaveRequest({requestUrl: this.originSaveUrl, saveRequestStatus: 'Error', originUrl: origin.url, saveTaskStatus: 'not created', responseStatus: 406}); makeOriginSaveRequest(origin.type, origin.url); cy.wait('@saveRequest').then(() => { checkAlertVisible('danger', saveCodeMsg['unknownError']); }); }); it('should display origin save info in the requests table', function() { cy.intercept('/save/requests/list/**', {fixture: 'origin-save'}) .as('saveRequestsList'); loadSaveRequestsListPage(); cy.get('tbody tr').then(rows => { let i = 0; for (const row of rows) { const cells = row.cells; const requestDateStr = new Date(this.originSaveJSON.data[i].save_request_date).toLocaleString(); const saveStatus = this.originSaveJSON.data[i].save_task_status; assert.equal($(cells[0]).text(), requestDateStr); assert.equal($(cells[1]).text(), this.originSaveJSON.data[i].visit_type); let html = ''; if (saveStatus === 'succeeded') { let browseOriginUrl = `${this.Urls.browse_origin()}?origin_url=${encodeURIComponent(this.originSaveJSON.data[i].origin_url)}`; browseOriginUrl += `&timestamp=${encodeURIComponent(this.originSaveJSON.data[i].visit_date)}`; html += `${this.originSaveJSON.data[i].origin_url}`; } else { html += this.originSaveJSON.data[i].origin_url; } html += ` `; html += ''; assert.equal($(cells[2]).html(), html); assert.equal($(cells[3]).text(), this.originSaveJSON.data[i].save_request_status); assert.equal($(cells[4]).text(), saveStatus); ++i; } }); }); + it('should display webhook icon when request was created from forge webhook receiver', function() { + const originUrl = 'https://git.example.org/example.git'; + const saveRequestData = genOriginSaveResponse({ + saveRequestStatus: 'accepted', + originUrl: originUrl, + saveTaskStatus: 'succeeded', + visitDate: null, + visitStatus: 'full', + fromWebhook: true + }); + const saveRequestsListData = { + 'recordsTotal': 1, + 'draw': 2, + 'recordsFiltered': 1, + 'data': [saveRequestData] + }; + + cy.intercept('/save/requests/list/**', {body: saveRequestsListData}) + .as('saveRequestsList'); + + loadSaveRequestsListPage(); + + cy.get('tbody tr').then(rows => { + const firstRowCells = rows[0].cells; + expect($(firstRowCells[5]).html()).to.contain.string('mdi-webhook'); + }); + }); + it('should not add timestamp to the browse origin URL is no visit date has been found', function() { const originUrl = 'https://git.example.org/example.git'; const saveRequestData = genOriginSaveResponse({ saveRequestStatus: 'accepted', originUrl: originUrl, saveTaskStatus: 'succeeded', visitDate: null, visitStatus: 'full' }); const saveRequestsListData = { 'recordsTotal': 1, 'draw': 2, 'recordsFiltered': 1, 'data': [saveRequestData] }; cy.intercept('/save/requests/list/**', {body: saveRequestsListData}) .as('saveRequestsList'); loadSaveRequestsListPage(); cy.get('tbody tr').then(rows => { const firstRowCells = rows[0].cells; const browseOriginUrl = `${this.Urls.browse_origin()}?origin_url=${encodeURIComponent(originUrl)}`; const browseOriginLink = `${originUrl}`; expect($(firstRowCells[2]).html()).to.have.string(browseOriginLink); }); }); it('should not add link to browse an origin when there is no visit status', function() { const originUrl = 'https://git.example.org/example.git'; const saveRequestData = genOriginSaveResponse({ saveRequestStatus: 'accepted', originUrl: originUrl, saveTaskStatus: 'succeeded', visitDate: null, visitStatus: null }); const saveRequestsListData = { 'recordsTotal': 1, 'draw': 2, 'recordsFiltered': 1, 'data': [saveRequestData] }; cy.intercept('/save/requests/list/**', {body: saveRequestsListData}) .as('saveRequestsList'); loadSaveRequestsListPage(); cy.get('tbody tr').then(rows => { const firstRowCells = rows[0].cells; const tooltip = 'origin was successfully loaded, waiting for data to be available in database'; const expectedContent = `${originUrl}`; expect($(firstRowCells[2]).html()).to.have.string(expectedContent); }); }); it('should display/close task info popover when clicking on the info button', function() { cy.intercept('/save/requests/list/**', {fixture: 'origin-save'}) .as('saveRequestsList'); cy.intercept('/save/task/info/**', {fixture: 'save-task-info'}) .as('saveTaskInfo'); loadSaveRequestsListPage(); cy.get('.swh-save-request-info') .eq(0) .click(); cy.wait('@saveTaskInfo'); cy.get('.swh-save-request-info-popover') .should('be.visible'); cy.get('.swh-save-request-info') .eq(0) .click(); cy.get('.swh-save-request-info-popover') .should('not.exist'); }); it('should hide task info popover when clicking on the close button', function() { cy.intercept('/save/requests/list/**', {fixture: 'origin-save'}) .as('saveRequestsList'); cy.intercept('/save/task/info/**', {fixture: 'save-task-info'}) .as('saveTaskInfo'); loadSaveRequestsListPage(); cy.get('.swh-save-request-info') .eq(0) .click(); cy.wait('@saveTaskInfo'); cy.get('.swh-save-request-info-popover') .should('be.visible'); cy.get('.swh-save-request-info-close') .click(); cy.get('.swh-save-request-info-popover') .should('not.exist'); }); it('should fill save request form when clicking on "Save again" button', function() { cy.intercept('/save/requests/list/**', {fixture: 'origin-save'}) .as('saveRequestsList'); loadSaveRequestsListPage(); cy.get('.swh-save-origin-again') .eq(0) .click(); cy.get('tbody tr').eq(0).then(row => { const cells = row[0].cells; cy.get('#swh-input-visit-type') .should('have.value', $(cells[1]).text()); cy.get('#swh-input-origin-url') .should('have.value', $(cells[2]).text().slice(0, -1)); }); }); it('should select correct visit type if possible when clicking on "Save again" button', function() { const originUrl = 'https://gitlab.inria.fr/solverstack/maphys/maphys/'; const badVisitType = 'hg'; const goodVisitType = 'git'; cy.intercept('/save/requests/list/**', {fixture: 'origin-save'}) .as('saveRequestsList'); stubSaveRequest({requestUrl: this.Urls.api_1_save_origin(badVisitType, originUrl), visitType: badVisitType, saveRequestStatus: 'accepted', originUrl: originUrl, saveTaskStatus: 'failed', visitStatus: 'failed', responseStatus: 200, errorMessage: saveCodeMsg['accepted']}); makeOriginSaveRequest(badVisitType, originUrl); loadSaveRequestsListPage(); cy.wait('@saveRequest').then(() => { cy.get('.swh-save-origin-again') .eq(0) .click(); cy.get('tbody tr').eq(0).then(row => { const cells = row[0].cells; cy.get('#swh-input-visit-type') .should('have.value', goodVisitType); cy.get('#swh-input-origin-url') .should('have.value', $(cells[2]).text().slice(0, -1)); }); }); }); it('should create save request for authenticated user', function() { cy.userLogin(); cy.visit(url); const originUrl = 'https://git.example.org/account/repo'; stubSaveRequest({requestUrl: this.Urls.api_1_save_origin('git', originUrl), saveRequestStatus: 'accepted', originUrl: origin.url, saveTaskStatus: 'not yet scheduled'}); makeOriginSaveRequest('git', originUrl); cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); it('should not show user requests filter checkbox for anonymous users', function() { cy.get('#swh-origin-save-requests-list-tab').click(); cy.get('#swh-save-requests-user-filter').should('not.exist'); }); it('should show user requests filter checkbox for authenticated users', function() { cy.userLogin(); cy.visit(url); cy.get('#swh-origin-save-requests-list-tab').click(); cy.get('#swh-save-requests-user-filter').should('exist'); }); it('should show only user requests when filter is activated', function() { cy.intercept('POST', '/api/1/origin/save/**') .as('saveRequest'); cy.intercept(this.Urls.origin_save_requests_list('all') + '**') .as('saveRequestsList'); const originAnonymousUser = 'https://some.git.server/project/'; const originAuthUser = 'https://other.git.server/project/'; // anonymous user creates a save request makeOriginSaveRequest('git', originAnonymousUser); cy.wait('@saveRequest'); // authenticated user creates another save request cy.userLogin(); cy.visit(url); makeOriginSaveRequest('git', originAuthUser); cy.wait('@saveRequest'); // user requests filter checkbox should be in the DOM loadSaveRequestsListPage(); cy.get('#swh-save-requests-user-filter').should('exist'); // check unfiltered user requests cy.get('tbody tr').then(rows => { expect(rows.length).to.eq(2); expect($(rows[0].cells[2]).text()).to.contain(originAuthUser); expect($(rows[1].cells[2]).text()).to.contain(originAnonymousUser); }); // activate filter and check filtered user requests cy.get('#swh-save-requests-user-filter') .click({force: true}); cy.wait('@saveRequestsList'); cy.get('tbody tr').then(rows => { expect(rows.length).to.eq(1); expect($(rows[0].cells[2]).text()).to.contain(originAuthUser); }); // deactivate filter and check unfiltered user requests cy.get('#swh-save-requests-user-filter') .click({force: true}); cy.wait('@saveRequestsList'); cy.get('tbody tr').then(rows => { expect(rows.length).to.eq(2); }); }); it('should list unprivileged visit types when not connected', function() { cy.visit(url); cy.get('#swh-input-visit-type').children('option').then(options => { const actual = [...options].map(o => o.value); expect(actual).to.deep.eq(anonymousVisitTypes); }); }); it('should list unprivileged visit types when connected as unprivileged user', function() { cy.userLogin(); cy.visit(url); cy.get('#swh-input-visit-type').children('option').then(options => { const actual = [...options].map(o => o.value); expect(actual).to.deep.eq(anonymousVisitTypes); }); }); it('should list privileged visit types when connected as ambassador', function() { cy.ambassadorLogin(); cy.visit(url); cy.get('#swh-input-visit-type').children('option').then(options => { const actual = [...options].map(o => o.value); expect(actual).to.deep.eq(allVisitTypes); }); }); it('should display extra inputs when dealing with \'archives\' visit type', function() { cy.ambassadorLogin(); cy.visit(url); for (const visitType of anonymousVisitTypes) { cy.get('#swh-input-visit-type').select(visitType); cy.get('.swh-save-origin-archives-form').should('not.be.visible'); } // this should display more inputs with the 'archives' type cy.get('#swh-input-visit-type').select('archives'); cy.get('.swh-save-origin-archives-form').should('be.visible'); }); it('should be allowed to submit \'archives\' save request when connected as ambassador', function() { const originUrl = 'https://github.com/chromium/chromium/tags'; const artifactUrl = 'https://github.com/chromium/chromium/archive/refs/tags/104.0.5106.1.tar.gz'; const artifactVersion = '104.0.5106.1'; stubSaveRequest({ requestUrl: this.Urls.api_1_save_origin('archives', originUrl), saveRequestStatus: 'accepted', originUrl: originUrl, saveTaskStatus: 'not yet scheduled' }); cy.ambassadorLogin(); cy.visit(url); // input new 'archives' information and submit cy.get('#swh-input-origin-url') .type(originUrl) .get('#swh-input-visit-type') .select('archives') .get('#swh-input-artifact-url-0') .type(artifactUrl) .get('#swh-input-artifact-version-0') .clear() .type(artifactVersion) .get('#swh-save-origin-form button[type=submit]') .click(); cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); it('should submit multiple artifacts for the archives visit type', function() { const originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf'; const artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz'; const artifactVersion = '1.1.4'; const artifact2Url = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.5.tar.gz'; const artifact2Version = '1.1.5'; cy.ambassadorLogin(); cy.visit(url); cy.get('#swh-input-origin-url') .type(originUrl) .get('#swh-input-visit-type') .select('archives'); // fill first artifact info cy.get('#swh-input-artifact-url-0') .type(artifactUrl) .get('#swh-input-artifact-version-0') .clear() .type(artifactVersion); // add new artifact form row cy.get('#swh-add-archive-artifact') .click(); // check new row is displayed cy.get('#swh-input-artifact-url-1') .should('exist'); // request removal of newly added row cy.get('#swh-remove-archive-artifact-1') .click(); // check row has been removed cy.get('#swh-input-artifact-url-1') .should('not.exist'); // add new artifact form row cy.get('#swh-add-archive-artifact') .click(); // fill second artifact info cy.get('#swh-input-artifact-url-1') .type(artifact2Url) .get('#swh-input-artifact-version-1') .clear() .type(artifact2Version); // setup request interceptor to check POST data and stub response cy.intercept('POST', this.Urls.api_1_save_origin('archives', originUrl), (req) => { expect(req.body).to.deep.equal({ archives_data: [ {artifact_url: artifactUrl, artifact_version: artifactVersion}, {artifact_url: artifact2Url, artifact_version: artifact2Version} ] }); req.reply(genOriginSaveResponse({ visitType: 'archives', saveRequestStatus: 'accepted', originUrl: originUrl, saveRequestDate: new Date(), saveTaskStatus: 'not yet scheduled', visitDate: null, visitStatus: null })); }).as('saveRequest'); // submit form cy.get('#swh-save-origin-form button[type=submit]') .click(); // submission should be successful cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); it('should autofill artifact version when pasting artifact url', function() { const originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf'; const artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz'; const artifactVersion = '3DLDF-1.1.4'; const artifact2Url = 'https://example.org/artifact/test/1.3.0.zip'; const artifact2Version = '1.3.0'; cy.ambassadorLogin(); cy.visit(url); cy.get('#swh-input-origin-url') .type(originUrl) .get('#swh-input-visit-type') .select('archives'); // fill first artifact info cy.get('#swh-input-artifact-url-0') .type(artifactUrl); // check autofilled version cy.get('#swh-input-artifact-version-0') .should('have.value', artifactVersion); // add new artifact form row cy.get('#swh-add-archive-artifact') .click(); // fill second artifact info cy.get('#swh-input-artifact-url-1') .type(artifact2Url); // check autofilled version cy.get('#swh-input-artifact-version-1') .should('have.value', artifact2Version); }); it('should use canonical URL for github repository to save', function() { const ownerRepo = 'BIC-MNI/mni_autoreg'; const canonicalOriginUrl = 'https://github.com/BIC-MNI/mni_autoreg'; // stub call to github Web API fetching canonical repo URL cy.intercept(`https://api.github.com/repos/${ownerRepo.toLowerCase()}`, (req) => { req.reply({html_url: canonicalOriginUrl}); }).as('ghWebApiRequest'); // stub save request creation with canonical URL of github repo cy.intercept('POST', this.Urls.api_1_save_origin('git', canonicalOriginUrl), (req) => { req.reply(genOriginSaveResponse({ visitType: 'git', saveRequestStatus: 'accepted', originUrl: canonicalOriginUrl, saveRequestDate: new Date(), saveTaskStatus: 'not yet scheduled', visitDate: null, visitStatus: null })); }).as('saveRequest'); for (const originUrl of ['https://github.com/BiC-MnI/MnI_AuToReG', 'https://github.com/BiC-MnI/MnI_AuToReG.git', 'https://github.com/BiC-MnI/MnI_AuToReG/', 'https://BiC-MnI.github.io/MnI_AuToReG/' ]) { // enter non canonical URL of github repo cy.get('#swh-input-origin-url') .clear() .type(originUrl); // submit form cy.get('#swh-save-origin-form button[type=submit]') .click(); // submission should be successful cy.wait('@ghWebApiRequest') .wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); } }); it('should switch tabs when playing with browser history', function() { cy.intercept('/save/requests/list/**', {fixture: 'origin-save'}); cy.intercept('/save/task/info/**', {fixture: 'save-task-info'}); cy.get('#swh-origin-save-request-help-tab') .should('have.class', 'active'); cy.get('#swh-origin-save-requests-list-tab') .click(); cy.get('#swh-origin-save-requests-list-tab') .should('have.class', 'active'); cy.go('back') .get('#swh-origin-save-request-help-tab') .should('have.class', 'active'); cy.go('forward') .get('#swh-origin-save-requests-list-tab') .should('have.class', 'active'); }); it('should not accept origin URL with password', function() { makeOriginSaveRequest('git', 'https://user:password@git.example.org/user/repo'); cy.get('.invalid-feedback') .should('contain', 'The origin url contains a password and cannot be accepted for security reasons'); }); it('should accept origin URL with username but without password', function() { cy.adminLogin(); cy.visit(url); const originUrl = 'https://user@git.example.org/user/repo'; stubSaveRequest({requestUrl: this.Urls.api_1_save_origin('git', originUrl), saveRequestStatus: 'accepted', originUrl: originUrl, saveTaskStatus: 'not yet scheduled'}); makeOriginSaveRequest('git', originUrl); cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); it('should accept origin URL with anonymous credentials', function() { cy.adminLogin(); cy.visit(url); const originUrl = 'https://anonymous:anonymous@git.example.org/user/repo'; stubSaveRequest({requestUrl: this.Urls.api_1_save_origin('git', originUrl), saveRequestStatus: 'accepted', originUrl: originUrl, saveTaskStatus: 'not yet scheduled'}); makeOriginSaveRequest('git', originUrl); cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); it('should accept origin URL with empty password', function() { cy.adminLogin(); cy.visit(url); const originUrl = 'https://anonymous:@git.example.org/user/repo'; stubSaveRequest({requestUrl: this.Urls.api_1_save_origin('git', originUrl), saveRequestStatus: 'accepted', originUrl: originUrl, saveTaskStatus: 'not yet scheduled'}); makeOriginSaveRequest('git', originUrl); cy.wait('@saveRequest').then(() => { checkAlertVisible('success', saveCodeMsg['success']); }); }); }); diff --git a/swh/web/save_code_now/assets/origin-save-admin.js b/swh/web/save_code_now/assets/origin-save-admin.js index 567b18d8..4ae8e107 100644 --- a/swh/web/save_code_now/assets/origin-save-admin.js +++ b/swh/web/save_code_now/assets/origin-save-admin.js @@ -1,400 +1,404 @@ /** * 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 {swhSpinnerSrc} from 'utils/constants'; import {csrfPost, getHumanReadableDate, handleFetchError, htmlAlert} from 'utils/functions'; let authorizedOriginTable; let unauthorizedOriginTable; let pendingSaveRequestsTable; let acceptedSaveRequestsTable; let rejectedSaveRequestsTable; function enableRowSelection(tableSel) { $(`${tableSel} tbody`).on('click', 'tr', function() { if ($(this).hasClass('selected')) { $(this).removeClass('selected'); $(tableSel).closest('.tab-pane').find('.swh-action-need-selection').prop('disabled', true); } else { $(`${tableSel} tr.selected`).removeClass('selected'); $(this).addClass('selected'); $(tableSel).closest('.tab-pane').find('.swh-action-need-selection').prop('disabled', false); } }); } export function initOriginSaveAdmin() { $(document).ready(() => { $.fn.dataTable.ext.errMode = 'throw'; authorizedOriginTable = $('#swh-authorized-origin-urls').DataTable({ serverSide: true, ajax: Urls.admin_origin_save_authorized_urls_list(), columns: [{data: 'url', name: 'url'}], scrollY: '50vh', scrollCollapse: true, info: false }); enableRowSelection('#swh-authorized-origin-urls'); swh.webapp.addJumpToPagePopoverToDataTable(authorizedOriginTable); unauthorizedOriginTable = $('#swh-unauthorized-origin-urls').DataTable({ serverSide: true, ajax: Urls.admin_origin_save_unauthorized_urls_list(), columns: [{data: 'url', name: 'url'}], scrollY: '50vh', scrollCollapse: true, info: false }); enableRowSelection('#swh-unauthorized-origin-urls'); swh.webapp.addJumpToPagePopoverToDataTable(unauthorizedOriginTable); const columnsData = [ { data: 'id', name: 'id', visible: false, searchable: false }, { data: 'save_request_date', name: 'request_date', render: getHumanReadableDate }, { data: 'visit_type', name: 'visit_type' }, { data: 'origin_url', name: 'origin_url', render: (data, type, row) => { if (type === 'display') { let html = ''; const sanitizedURL = $.fn.dataTable.render.text().display(data); if (row.save_task_status === 'succeeded') { let browseOriginUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(sanitizedURL)}`; if (row.visit_date) { browseOriginUrl += `&timestamp=${encodeURIComponent(row.visit_date)}`; } html += `${sanitizedURL}`; } else { html += sanitizedURL; } html += ` ` + ''; return html; } return data; } } ]; pendingSaveRequestsTable = $('#swh-origin-save-pending-requests').DataTable({ serverSide: true, processing: true, language: { processing: `` }, ajax: Urls.origin_save_requests_list('pending'), searchDelay: 1000, columns: columnsData, scrollY: '50vh', scrollCollapse: true, order: [[0, 'desc']], responsive: { details: { type: 'none' } } }); enableRowSelection('#swh-origin-save-pending-requests'); swh.webapp.addJumpToPagePopoverToDataTable(pendingSaveRequestsTable); columnsData.push({ name: 'info', render: (data, type, row) => { + let info = ''; if (row.save_task_status === 'succeeded' || row.save_task_status === 'failed' || row.note != null) { - return ``; - } else { - return ''; } + if (row.from_webhook) { + info += ``; + } + return info; } }); rejectedSaveRequestsTable = $('#swh-origin-save-rejected-requests').DataTable({ serverSide: true, processing: true, language: { processing: `` }, ajax: Urls.origin_save_requests_list('rejected'), searchDelay: 1000, columns: columnsData, scrollY: '50vh', scrollCollapse: true, order: [[0, 'desc']], responsive: { details: { type: 'none' } } }); enableRowSelection('#swh-origin-save-rejected-requests'); swh.webapp.addJumpToPagePopoverToDataTable(rejectedSaveRequestsTable); columnsData.splice(columnsData.length - 1, 0, { data: 'save_task_status', name: 'save_task_status' }); acceptedSaveRequestsTable = $('#swh-origin-save-accepted-requests').DataTable({ serverSide: true, processing: true, language: { processing: `` }, ajax: Urls.origin_save_requests_list('accepted'), searchDelay: 1000, columns: columnsData, scrollY: '50vh', scrollCollapse: true, order: [[0, 'desc']], responsive: { details: { type: 'none' } } }); enableRowSelection('#swh-origin-save-accepted-requests'); swh.webapp.addJumpToPagePopoverToDataTable(acceptedSaveRequestsTable); $('#swh-authorized-origins-tab').on('shown.bs.tab', () => { authorizedOriginTable.draw(); }); $('#swh-unauthorized-origins-tab').on('shown.bs.tab', () => { unauthorizedOriginTable.draw(); }); $('#swh-save-requests-pending-tab').on('shown.bs.tab', () => { pendingSaveRequestsTable.draw(); }); $('#swh-save-requests-accepted-tab').on('shown.bs.tab', () => { acceptedSaveRequestsTable.draw(); }); $('#swh-save-requests-rejected-tab').on('shown.bs.tab', () => { rejectedSaveRequestsTable.draw(); }); $('#swh-save-requests-pending-tab').click(() => { pendingSaveRequestsTable.ajax.reload(null, false); }); $('#swh-save-requests-accepted-tab').click(() => { acceptedSaveRequestsTable.ajax.reload(null, false); }); $('#swh-save-requests-rejected-tab').click(() => { rejectedSaveRequestsTable.ajax.reload(null, false); }); $('body').on('click', e => { if ($(e.target).parents('.popover').length > 0) { e.stopPropagation(); } else if ($(e.target).parents('.swh-save-request-info').length === 0) { $('.swh-save-request-info').popover('dispose'); } }); }); } export async function addAuthorizedOriginUrl() { const originUrl = $('#swh-authorized-url-prefix').val(); const addOriginUrl = Urls.admin_origin_save_add_authorized_url(originUrl); try { const response = await csrfPost(addOriginUrl); handleFetchError(response); authorizedOriginTable.row.add({'url': originUrl}).draw(); $('.swh-add-authorized-origin-status').html( htmlAlert('success', 'The origin url prefix has been successfully added in the authorized list.', true) ); } catch (_) { $('.swh-add-authorized-origin-status').html( htmlAlert('warning', 'The provided origin url prefix is already registered in the authorized list.', true) ); } } export async function removeAuthorizedOriginUrl() { const originUrl = $('#swh-authorized-origin-urls tr.selected').text(); if (originUrl) { const removeOriginUrl = Urls.admin_origin_save_remove_authorized_url(originUrl); try { const response = await csrfPost(removeOriginUrl); handleFetchError(response); authorizedOriginTable.row('.selected').remove().draw(); } catch (_) {} } } export async function addUnauthorizedOriginUrl() { const originUrl = $('#swh-unauthorized-url-prefix').val(); const addOriginUrl = Urls.admin_origin_save_add_unauthorized_url(originUrl); try { const response = await csrfPost(addOriginUrl); handleFetchError(response); unauthorizedOriginTable.row.add({'url': originUrl}).draw(); $('.swh-add-unauthorized-origin-status').html( htmlAlert('success', 'The origin url prefix has been successfully added in the unauthorized list.', true) ); } catch (_) { $('.swh-add-unauthorized-origin-status').html( htmlAlert('warning', 'The provided origin url prefix is already registered in the unauthorized list.', true) ); } } export async function removeUnauthorizedOriginUrl() { const originUrl = $('#swh-unauthorized-origin-urls tr.selected').text(); if (originUrl) { const removeOriginUrl = Urls.admin_origin_save_remove_unauthorized_url(originUrl); try { const response = await csrfPost(removeOriginUrl); handleFetchError(response); unauthorizedOriginTable.row('.selected').remove().draw(); } catch (_) {}; } } export function acceptOriginSaveRequest() { const selectedRow = pendingSaveRequestsTable.row('.selected'); if (selectedRow.length) { const acceptOriginSaveRequestCallback = async() => { const rowData = selectedRow.data(); const acceptSaveRequestUrl = Urls.admin_origin_save_request_accept(rowData['visit_type'], rowData['origin_url']); await csrfPost(acceptSaveRequestUrl); pendingSaveRequestsTable.ajax.reload(null, false); }; swh.webapp.showModalConfirm( 'Accept origin save request ?', 'Are you sure to accept this origin save request ?', acceptOriginSaveRequestCallback); } } const rejectModalHtml = `
`; export function rejectOriginSaveRequest() { const selectedRow = pendingSaveRequestsTable.row('.selected'); const rowData = selectedRow.data(); if (selectedRow.length) { const rejectOriginSaveRequestCallback = async() => { $('#swh-web-modal-html').modal('hide'); const rejectSaveRequestUrl = Urls.admin_origin_save_request_reject( rowData['visit_type'], rowData['origin_url']); await csrfPost(rejectSaveRequestUrl, {}, JSON.stringify({note: $('#swh-rejection-text').val()})); pendingSaveRequestsTable.ajax.reload(null, false); }; let currentRejectionReason = 'custom'; const rejectionTexts = {}; swh.webapp.showModalHtml('Reject origin save request ?', rejectModalHtml); $('#swh-rejection-reason').on('change', (event) => { // backup current textarea value rejectionTexts[currentRejectionReason] = $('#swh-rejection-text').val(); currentRejectionReason = event.target.value; let newRejectionText = ''; if (rejectionTexts.hasOwnProperty(currentRejectionReason)) { // restore previous textarea value newRejectionText = rejectionTexts[currentRejectionReason]; } else { // fill textarea with default text according to rejection type if (currentRejectionReason === 'invalid-origin') { newRejectionText = `The origin with URL ${rowData['origin_url']} is not ` + `a link to a ${rowData['visit_type']} repository.`; } else if (currentRejectionReason === 'invalid-origin-type') { newRejectionText = `The origin with URL ${rowData['origin_url']} is not ` + `of type ${rowData['visit_type']}.`; } else if (currentRejectionReason === 'origin-not-found') { newRejectionText = `The origin with URL ${rowData['origin_url']} cannot be found.`; } } $('#swh-rejection-text').val(newRejectionText); }); $('#swh-rejection-form').on('submit', (event) => { event.preventDefault(); event.stopPropagation(); // ensure confirmation modal will be displayed above the html modal $('#swh-web-modal-html').css('z-index', 4000); swh.webapp.showModalConfirm( 'Reject origin save request ?', 'Are you sure to reject this origin save request ?', rejectOriginSaveRequestCallback); }); } } function removeOriginSaveRequest(requestTable) { const selectedRow = requestTable.row('.selected'); if (selectedRow.length) { const requestId = selectedRow.data()['id']; const removeOriginSaveRequestCallback = async() => { const removeSaveRequestUrl = Urls.admin_origin_save_request_remove(requestId); await csrfPost(removeSaveRequestUrl); requestTable.ajax.reload(null, false); }; swh.webapp.showModalConfirm( 'Remove origin save request ?', 'Are you sure to remove this origin save request ?', removeOriginSaveRequestCallback); } } export function removePendingOriginSaveRequest() { removeOriginSaveRequest(pendingSaveRequestsTable); } export function removeAcceptedOriginSaveRequest() { removeOriginSaveRequest(acceptedSaveRequestsTable); } export function removeRejectedOriginSaveRequest() { removeOriginSaveRequest(rejectedSaveRequestsTable); } diff --git a/swh/web/save_code_now/assets/origin-save.js b/swh/web/save_code_now/assets/origin-save.js index ba64bc27..1907fb64 100644 --- a/swh/web/save_code_now/assets/origin-save.js +++ b/swh/web/save_code_now/assets/origin-save.js @@ -1,546 +1,551 @@ /** * 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 {swhSpinnerSrc} from 'utils/constants'; import { csrfPost, getCanonicalOriginURL, getHumanReadableDate, handleFetchError, htmlAlert, isGitRepoUrl, validateUrl } from 'utils/functions'; import userRequestsFilterCheckboxFn from 'utils/requests-filter-checkbox.ejs'; import artifactFormRowTemplate from './artifact-form-row.ejs'; let saveRequestsTable; async function originSaveRequest( originType, originUrl, extraData, acceptedCallback, pendingCallback, errorCallback ) { // Actually trigger the origin save request const addSaveOriginRequestUrl = Urls.api_1_save_origin(originType, originUrl); $('.swh-processing-save-request').css('display', 'block'); let headers = {}; let body = null; if (extraData !== {}) { body = JSON.stringify(extraData); headers = { 'Content-Type': 'application/json' }; }; try { const response = await csrfPost(addSaveOriginRequestUrl, headers, body); handleFetchError(response); const data = await response.json(); $('.swh-processing-save-request').css('display', 'none'); if (data.save_request_status === 'accepted') { acceptedCallback(); } else { pendingCallback(); } } catch (response) { $('.swh-processing-save-request').css('display', 'none'); const errorData = await response.json(); errorCallback(response.status, errorData); }; } function addArtifactVersionAutofillHandler(formId) { // autofill artifact version input with the filename from // the artifact url without extensions $(`#swh-input-artifact-url-${formId}`).on('input', function(event) { const artifactUrl = $(this).val().trim(); let filename = artifactUrl.split('/').slice(-1)[0]; if (filename !== artifactUrl) { filename = filename.replace(/tar.*$/, 'tar'); const filenameNoExt = filename.split('.').slice(0, -1).join('.'); const artifactVersion = $(`#swh-input-artifact-version-${formId}`); if (filenameNoExt !== filename) { artifactVersion.val(filenameNoExt); } } }); } export function maybeRequireExtraInputs() { // Read the actual selected value and depending on the origin type, display some extra // inputs or hide them. This makes the extra inputs disabled when not displayed. const originType = $('#swh-input-visit-type').val(); let display = 'none'; let disabled = true; if (originType === 'archives') { display = 'flex'; disabled = false; } $('.swh-save-origin-archives-form').css('display', display); if (!disabled) { // help paragraph must have block display for proper rendering $('#swh-save-origin-archives-help').css('display', 'block'); } $('.swh-save-origin-archives-form .form-control').prop('disabled', disabled); if (originType === 'archives' && $('.swh-save-origin-archives-form').length === 1) { // insert first artifact row when the archives visit type is selected for the first time $('.swh-save-origin-archives-form').last().after( artifactFormRowTemplate({deletableRow: false, formId: 0})); addArtifactVersionAutofillHandler(0); } } export function addArtifactFormRow() { const formId = $('.swh-save-origin-artifact-form').length; $('.swh-save-origin-artifact-form').last().after( artifactFormRowTemplate({ deletableRow: true, formId: formId }) ); addArtifactVersionAutofillHandler(formId); } export function deleteArtifactFormRow(event) { $(event.target).closest('.swh-save-origin-artifact-form').remove(); } const saveRequestCheckboxId = 'swh-save-requests-user-filter'; const userRequestsFilterCheckbox = userRequestsFilterCheckboxFn({ 'inputId': saveRequestCheckboxId, 'checked': false // no filtering by default on that view }); export function initOriginSave() { $(document).ready(() => { $.fn.dataTable.ext.errMode = 'none'; // set git as the default value as before $('#swh-input-visit-type').val('git'); saveRequestsTable = $('#swh-origin-save-requests') .on('error.dt', (e, settings, techNote, message) => { $('#swh-origin-save-request-list-error').text('An error occurred while retrieving the save requests list'); console.log(message); }) .DataTable({ serverSide: true, processing: true, language: { processing: `` }, ajax: { url: Urls.origin_save_requests_list('all'), data: (d) => { if (swh.webapp.isUserLoggedIn() && $(`#${saveRequestCheckboxId}`).prop('checked')) { d.user_requests_only = '1'; } } }, searchDelay: 1000, // see https://datatables.net/examples/advanced_init/dom_toolbar.html and the comments section // this option customizes datatables UI components by adding an extra checkbox above the table // while keeping bootstrap layout 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>>', fnInitComplete: function() { if (swh.webapp.isUserLoggedIn()) { $('div.user-requests-filter').html(userRequestsFilterCheckbox); $(`#${saveRequestCheckboxId}`).on('change', () => { saveRequestsTable.draw(); }); } }, columns: [ { data: 'save_request_date', name: 'request_date', render: getHumanReadableDate }, { data: 'visit_type', name: 'visit_type' }, { data: 'origin_url', name: 'origin_url', render: (data, type, row) => { if (type === 'display') { let html = ''; const sanitizedURL = $.fn.dataTable.render.text().display(data); if (row.save_task_status === 'succeeded') { if (row.visit_status === 'full' || row.visit_status === 'partial') { let browseOriginUrl = `${Urls.browse_origin()}?origin_url=${encodeURIComponent(sanitizedURL)}`; if (row.visit_date) { browseOriginUrl += `&timestamp=${encodeURIComponent(row.visit_date)}`; } html += `${sanitizedURL}`; } else { const tooltip = 'origin was successfully loaded, waiting for data to be available in database'; html += `${sanitizedURL}`; } } else { html += sanitizedURL; } html += ` ` + ''; return html; } return data; } }, { data: 'save_request_status', name: 'status' }, { data: 'save_task_status', name: 'loading_task_status' }, { name: 'info', render: (data, type, row) => { + let info = ''; if (row.save_task_status === 'succeeded' || row.save_task_status === 'failed' || row.note != null) { - return ``; - } else { - return ''; } + if (row.from_webhook) { + info += ``; + } + return info; + } }, { render: (data, type, row) => { if (row.save_request_status === 'accepted') { const saveAgainButton = ''; return saveAgainButton; } else { return ''; } } } ], scrollY: '50vh', scrollCollapse: true, order: [[0, 'desc']], responsive: { details: { type: 'none' } } }); swh.webapp.addJumpToPagePopoverToDataTable(saveRequestsTable); if (window.location.pathname === Urls.origin_save() && window.location.hash === '#requests') { // Keep old URLs to the save list working window.location = Urls.origin_save_list(); } else if ($('#swh-origin-save-requests')) { saveRequestsTable.draw(); } const saveRequestAcceptedAlert = htmlAlert( 'success', 'The "save code now" request has been accepted and will be processed as soon as possible.', true ); const saveRequestPendingAlert = htmlAlert( 'warning', 'The "save code now" request has been put in pending state and may be accepted for processing after manual review.', true ); const saveRequestRateLimitedAlert = htmlAlert( 'danger', 'The rate limit for "save code now" requests has been reached. Please try again later.', true ); const saveRequestUnknownErrorAlert = htmlAlert( 'danger', 'An unexpected error happened when submitting the "save code now request".', true ); $('#swh-save-origin-form').submit(async event => { event.preventDefault(); event.stopPropagation(); $('.alert').alert('close'); if (event.target.checkValidity()) { $(event.target).removeClass('was-validated'); const originType = $('#swh-input-visit-type').val(); let originUrl = $('#swh-input-origin-url').val(); originUrl = await getCanonicalOriginURL(originUrl); // read the extra inputs for the 'archives' type const extraData = {}; if (originType === 'archives') { extraData['archives_data'] = []; for (let i = 0; i < $('.swh-save-origin-artifact-form').length; ++i) { extraData['archives_data'].push({ 'artifact_url': $(`#swh-input-artifact-url-${i}`).val(), 'artifact_version': $(`#swh-input-artifact-version-${i}`).val() }); } } originSaveRequest(originType, originUrl, extraData, () => $('#swh-origin-save-request-status').html(saveRequestAcceptedAlert), () => $('#swh-origin-save-request-status').html(saveRequestPendingAlert), (statusCode, errorData) => { $('#swh-origin-save-request-status').css('color', 'red'); if (statusCode === 403) { const errorAlert = htmlAlert('danger', `Error: ${errorData['reason']}`); $('#swh-origin-save-request-status').html(errorAlert); } else if (statusCode === 429) { $('#swh-origin-save-request-status').html(saveRequestRateLimitedAlert); } else if (statusCode === 400) { const errorAlert = htmlAlert('danger', errorData['reason']); $('#swh-origin-save-request-status').html(errorAlert); } else { $('#swh-origin-save-request-status').html(saveRequestUnknownErrorAlert); } }); } else { $(event.target).addClass('was-validated'); } }); $('#swh-show-origin-save-requests-list').on('click', (event) => { event.preventDefault(); $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show'); }); $('#swh-input-origin-url').on('input', function(event) { const originUrl = $(this).val().trim(); $(this).val(originUrl); $('#swh-input-visit-type option').each(function() { const val = $(this).val(); if (val && originUrl.includes(val)) { $(this).prop('selected', true); // origin URL input need to be validated once new visit type set validateSaveOriginUrl($('#swh-input-origin-url')[0]); } }); }); if (window.location.hash === '#requests') { $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show'); } $(window).on('hashchange', () => { if (window.location.hash === '#requests') { $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show'); } else { $('.nav-tabs a[href="#swh-origin-save-requests-create"]').tab('show'); } }); }); } export function validateSaveOriginUrl(input) { const originType = $('#swh-input-visit-type').val(); const allowedProtocols = ['http:', 'https:', 'svn:', 'git:', 'rsync:', 'pserver:', 'ssh:', 'bzr:']; const originUrl = validateUrl(input.value.trim(), allowedProtocols); let validUrl = originUrl !== null; if (validUrl && originType === 'git') { validUrl = isGitRepoUrl(originUrl); } let customValidity = ''; if (validUrl) { if ((originUrl.password !== '' && originUrl.password !== 'anonymous')) { customValidity = 'The origin url contains a password and cannot be accepted for security reasons'; } } else { customValidity = 'The origin url is not valid or does not reference a code repository'; } input.setCustomValidity(customValidity); $(input).siblings('.invalid-feedback').text(customValidity); } export function initTakeNewSnapshot() { const newSnapshotRequestAcceptedAlert = htmlAlert( 'success', 'The "take new snapshot" request has been accepted and will be processed as soon as possible.', true ); const newSnapshotRequestPendingAlert = htmlAlert( 'warning', 'The "take new snapshot" request has been put in pending state and may be accepted for processing after manual review.', true ); const newSnapshotRequestRateLimitAlert = htmlAlert( 'danger', 'The rate limit for "take new snapshot" requests has been reached. Please try again later.', true ); const newSnapshotRequestUnknownErrorAlert = htmlAlert( 'danger', 'An unexpected error happened when submitting the "save code now request".', true ); $(document).ready(() => { $('#swh-take-new-snapshot-form').submit(event => { event.preventDefault(); event.stopPropagation(); const originType = $('#swh-input-visit-type').val(); const originUrl = $('#swh-input-origin-url').val(); const extraData = {}; originSaveRequest(originType, originUrl, extraData, () => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestAcceptedAlert), () => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestPendingAlert), (statusCode, errorData) => { $('#swh-take-new-snapshot-request-status').css('color', 'red'); if (statusCode === 403) { const errorAlert = htmlAlert('danger', `Error: ${errorData['detail']}`, true); $('#swh-take-new-snapshot-request-status').html(errorAlert); } else if (statusCode === 429) { $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestRateLimitAlert); } else { $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestUnknownErrorAlert); } }); }); }); } export function formatValuePerType(type, value) { // Given some typed value, format and return accordingly formatted value const mapFormatPerTypeFn = { 'json': (v) => JSON.stringify(v, null, 2), 'date': (v) => new Date(v).toLocaleString(), 'raw': (v) => v, 'duration': (v) => v + ' seconds' }; return value === null ? null : mapFormatPerTypeFn[type](value); } export async function displaySaveRequestInfo(event, saveRequestId) { event.stopPropagation(); const saveRequestTaskInfoUrl = Urls.origin_save_task_info(saveRequestId); // close popover when clicking again on the info icon if ($(event.target).data('bs.popover')) { $(event.target).popover('dispose'); return; } $('.swh-save-request-info').popover('dispose'); $(event.target).popover({ animation: false, boundary: 'viewport', container: 'body', title: 'Save request task information ' + '`, content: `

Fetching task information ...

`, html: true, placement: 'left', sanitizeFn: swh.webapp.filterXSS }); $(event.target).on('shown.bs.popover', function() { const popoverId = $(this).attr('aria-describedby'); $(`#${popoverId} .mdi-close`).click(() => { $(this).popover('dispose'); }); }); $(event.target).popover('show'); const response = await fetch(saveRequestTaskInfoUrl); const saveRequestTaskInfo = await response.json(); let content; if ($.isEmptyObject(saveRequestTaskInfo)) { content = 'Not available'; } else if (saveRequestTaskInfo.note != null) { content = `
${saveRequestTaskInfo.note}
`; } else { const saveRequestInfo = []; const taskData = { 'Type': ['raw', 'type'], 'Visit status': ['raw', 'visit_status'], 'Arguments': ['json', 'arguments'], 'Id': ['raw', 'id'], 'Backend id': ['raw', 'backend_id'], 'Scheduling date': ['date', 'scheduled'], 'Start date': ['date', 'started'], 'Completion date': ['date', 'ended'], 'Duration': ['duration', 'duration'], 'Runner': ['raw', 'worker'], 'Log': ['raw', 'message'] }; for (const [title, [type, property]] of Object.entries(taskData)) { if (saveRequestTaskInfo.hasOwnProperty(property)) { saveRequestInfo.push({ key: title, value: formatValuePerType(type, saveRequestTaskInfo[property]) }); } } content = ''; for (const info of saveRequestInfo) { content += ``; } content += '
'; } $('.swh-popover').html(content); $(event.target).popover('update'); } export function fillSaveRequestFormAndScroll(visitType, originUrl) { $('#swh-input-origin-url').val(originUrl); let originTypeFound = false; $('#swh-input-visit-type option').each(function() { const val = $(this).val(); if (val && originUrl.includes(val)) { $(this).prop('selected', true); originTypeFound = true; } }); if (!originTypeFound) { $('#swh-input-visit-type option').each(function() { const val = $(this).val(); if (val === visitType) { $(this).prop('selected', true); } }); } window.scrollTo(0, 0); }