Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

softwareRequirements: Validate values are URL + make isUrl stricter #57

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 107 additions & 1 deletion cypress/integration/special_fields.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (C) 2020 The Software Heritage developers
* Copyright (C) 2020-2024 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
Expand All @@ -10,6 +10,7 @@
*/

"use strict";

describe('Funder id', function() {
it('can be exported', function() {
cy.get('#name').type('My Test Software');
Expand Down Expand Up @@ -88,3 +89,108 @@ describe('Funder name', function() {
});
});

describe('Software requirements', function() {
it('can be exported as multiple values', function() {
cy.get('#name').type('My Test Software');

cy.get('#softwareRequirements').type('https://www.gtk.org\nhttps://github.com/anholt/libepoxy\nhttps://github.com/GNOME/libxml2');

cy.get('#generateCodemetaV2').click();

cy.get('#errorMessage').should('have.text', '');
cy.get('#codemetaText').then((elem) => JSON.parse(elem.text()))
.should('deep.equal', {
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
"type": "SoftwareSourceCode",
"name": "My Test Software",
"softwareRequirements": [
"https://www.gtk.org",
"https://github.com/anholt/libepoxy",
"https://github.com/GNOME/libxml2",
]
});
});

it('can be imported from multiple values', function() {
cy.get('#codemetaText').then((elem) =>
elem.text(JSON.stringify({
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
"@type": "SoftwareSourceCode",
"name": "My Test Software",
"softwareRequirements": [
"https://www.gtk.org",
"https://github.com/anholt/libepoxy",
"https://github.com/GNOME/libxml2",
]
}))
);
cy.get('#importCodemeta').click();

cy.get('#softwareRequirements').should('have.value', 'https://www.gtk.org\nhttps://github.com/anholt/libepoxy\nhttps://github.com/GNOME/libxml2');
});

it('can be exported to a single URI', function() {
cy.get('#name').type('My Test Software');

cy.get('#softwareRequirements').type('https://github.com/GNOME/libxml2');

cy.get('#generateCodemetaV2').click();

cy.get('#errorMessage').should('have.text', '');
cy.get('#codemetaText').then((elem) => JSON.parse(elem.text()))
.should('deep.equal', {
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
"type": "SoftwareSourceCode",
"name": "My Test Software",
"softwareRequirements": "https://github.com/GNOME/libxml2",
});
});

it('can be exported despite a trailing newline', function() {
cy.get('#name').type('My Test Software');

cy.get('#softwareRequirements').type('https://github.com/GNOME/libxml2\n');

cy.get('#generateCodemetaV2').click();

cy.get('#errorMessage').should('have.text', '');
cy.get('#codemetaText').then((elem) => JSON.parse(elem.text()))
.should('deep.equal', {
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
"type": "SoftwareSourceCode",
"name": "My Test Software",
"softwareRequirements": "https://github.com/GNOME/libxml2",
});
});

it('can be imported from a single URI', function() {
cy.get('#codemetaText').then((elem) =>
elem.text(JSON.stringify({
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
"@type": "SoftwareSourceCode",
"name": "My Test Software",
"softwareRequirements": "https://github.com/GNOME/libxml2",
}))
);
cy.get('#importCodemeta').click();

cy.get('#softwareRequirements').should('have.value', 'https://github.com/GNOME/libxml2');
});

it('cannot be exported as text', function() {
cy.get('#name').type('My Test Software');

cy.get('#softwareRequirements').type('libxml2');

cy.get('#generateCodemetaV2').click();

cy.get('#errorMessage').should('have.text', 'Invalid URL in field "softwareRequirements": "libxml2"');
cy.get('#codemetaText').then((elem) => JSON.parse(elem.text()))
.should('deep.equal', {
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
"type": "SoftwareSourceCode",
"name": "My Test Software",
"softwareRequirements": [],
});
});
});
13 changes: 13 additions & 0 deletions cypress/integration/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,19 @@ describe('URLs validation', function() {
cy.get('#errorMessage').should('have.text', 'Invalid URL in field "codeRepository": "foo"');
});

it('errors on invalid URL that Javascript considers to be valid', function() {
cy.get('#codemetaText').then((elem) =>
elem.text(JSON.stringify({
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
"@type": "SoftwareSourceCode",
"codeRepository": "foo: bar",
}))
);
cy.get('#validateCodemeta').click();

cy.get('#errorMessage').should('have.text', 'Invalid URL in field "codeRepository": "foo: bar"');
});

it('errors on non-string instead of URL', function() {
cy.get('#codemetaText').then((elem) =>
elem.text(JSON.stringify({
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ <h1>CodeMeta generator v3.0</h1>
<textarea rows="4" cols="50"
name="softwareRequirements" id="softwareRequirements"
placeholder=
"Python 3.4
"https://www.python.org/downloads/release/python-3130/
https://github.com/psf/requests"></textarea>
</fieldset>

Expand Down
44 changes: 39 additions & 5 deletions js/codemeta_generation.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,14 @@ const directCodemetaFields = [
'referencePublication'
];

// Tuples of codemeta property, character joining/splitting items in a textarea, and optionally
// post-processing for deserialization and pre-processing for deserialization
const splittedCodemetaFields = [
['keywords', ','],
['programmingLanguage', ','],
['runtimePlatform', ','],
['operatingSystem', ','],
['softwareRequirements', '\n'],
['softwareRequirements', '\n', generateUri, importUri],
['relatedLink', '\n'],
]

Expand Down Expand Up @@ -136,6 +138,17 @@ function generateBlankNodeId(customId) {
return `_:${customId}`;
}

// Unambiguously converts a free-form text field that might be a URI or actual free text
// in JSON-LD
function generateUri(fieldName, text) {
if (isUrl(text)) {
return {"@id": text};
} else {
setError(`Invalid URL in field "${fieldName}": "${text}"`);
return undefined;
}
}

function generateShortOrg(fieldName) {
var affiliation = getIfSet(fieldName);
if (affiliation !== undefined) {
Expand Down Expand Up @@ -246,9 +259,15 @@ async function buildExpandedDocWithAllContexts() {
splittedCodemetaFields.forEach(function (item, index) {
const id = item[0];
const separator = item[1];
const value = getIfSet('#' + id);
const serializer = item[2];
const deserializer = item[3];
let value = getIfSet('#' + id);
if (value !== undefined) {
doc[id] = value.split(separator).map(trimSpaces);
value = value.split(separator).map(trimSpaces).filter((item) => item != "");
if (serializer !== undefined) {
value = value.map((item) => serializer(id, item));
}
doc[id] = value;
}
});

Expand All @@ -275,6 +294,8 @@ async function generateCodemeta(codemetaVersion = "2.0") {
var inputForm = document.querySelector('#inputForm');
var codemetaText, errorHTML;

setError("");

if (inputForm.checkValidity()) {
// Expand document with all contexts before compacting
// to allow generating property from any context
Expand All @@ -285,12 +306,11 @@ async function generateCodemeta(codemetaVersion = "2.0") {
}
else {
codemetaText = "";
errorHTML = "invalid input (see error above)";
setError("invalid input (see error above)");
inputForm.reportValidity();
}

document.querySelector('#codemetaText').innerText = codemetaText;
setError(errorHTML);


// Run validator on the exported value, for extra validation.
Expand All @@ -307,6 +327,11 @@ async function generateCodemeta(codemetaVersion = "2.0") {
}
}

// Imports a field that can be either URI or free-form text into a free-form text field
function importUri(fieldName, doc) {
return doc;
}

// Imports a single field (name or @id) from an Organization.
function importShortOrg(fieldName, doc) {
if (doc !== undefined) {
Expand Down Expand Up @@ -433,10 +458,19 @@ async function importCodemeta() {
splittedCodemetaFields.forEach(function (item, index) {
const id = item[0];
const separator = item[1];
const serializer = item[2];
const deserializer = item[3];
let value = doc[id];
if (value !== undefined) {
if (Array.isArray(value)) {
if (deserializer !== undefined) {
value = value.map((item) => deserializer(id, item));
}
value = value.join(separator);
} else {
if (deserializer !== undefined) {
value = deserializer(id, value);
}
}
setIfDefined('#' + id, value);
}
Expand Down
6 changes: 5 additions & 1 deletion js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ function trimSpaces(s) {
// From https://stackoverflow.com/a/43467144
function isUrl(s) {
try {
new URL(s);
const url = new URL(s);
if (url.origin == "null") {
// forbids "foo: bar" as a URL, for example
return false;
}
return true;
} catch (e) {
return false;
Expand Down
2 changes: 1 addition & 1 deletion js/validation/things.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ var softwareFieldValidators = {
"processorRequirements": validateTexts,
"releaseNotes": validateTextsOrUrls,
"softwareHelp": validateCreativeWorks,
"softwareRequirements": noValidation, // TODO: validate SoftwareSourceCode
"softwareRequirements": validateUrls,
"softwareVersion": validateText, // TODO?
"storageRequirements": validateTextsOrUrls,
"supportingData": noValidation, // TODO
Expand Down