Skip to content

Commit e29a32d

Browse files
committed
softwareRequirements: Validate values are URL + make isUrl stricter
Without this check, users would be allowed to write free text even though Codemeta forbids it. We only noticed this when jsonld.js started rewriting 'foo: bar' to ' bar' because it was parsing 'foo:' as an invalid URI scheme and stripping it.
1 parent 29f932f commit e29a32d

File tree

4 files changed

+132
-6
lines changed

4 files changed

+132
-6
lines changed

cypress/integration/special_fields.js

+90-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (C) 2020 The Software Heritage developers
2+
* Copyright (C) 2020-2024 The Software Heritage developers
33
* See the AUTHORS file at the top-level directory of this distribution
44
* License: GNU Affero General Public License version 3, or any later version
55
* See top-level LICENSE file for more information
@@ -10,6 +10,7 @@
1010
*/
1111

1212
"use strict";
13+
1314
describe('Funder id', function() {
1415
it('can be exported', function() {
1516
cy.get('#name').type('My Test Software');
@@ -88,3 +89,91 @@ describe('Funder name', function() {
8889
});
8990
});
9091

92+
describe('Software requirements', function() {
93+
it('can be exported as multiple values', function() {
94+
cy.get('#name').type('My Test Software');
95+
96+
cy.get('#softwareRequirements').type('https://www.gtk.org\nhttps://github.com/anholt/libepoxy\nhttps://github.com/GNOME/libxml2');
97+
98+
cy.get('#generateCodemetaV2').click();
99+
100+
cy.get('#errorMessage').should('have.text', '');
101+
cy.get('#codemetaText').then((elem) => JSON.parse(elem.text()))
102+
.should('deep.equal', {
103+
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
104+
"type": "SoftwareSourceCode",
105+
"name": "My Test Software",
106+
"softwareRequirements": [
107+
"https://www.gtk.org",
108+
"https://github.com/anholt/libepoxy",
109+
"https://github.com/GNOME/libxml2",
110+
]
111+
});
112+
});
113+
114+
it('can be imported from multiple values', function() {
115+
cy.get('#codemetaText').then((elem) =>
116+
elem.text(JSON.stringify({
117+
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
118+
"@type": "SoftwareSourceCode",
119+
"name": "My Test Software",
120+
"softwareRequirements": [
121+
"https://www.gtk.org",
122+
"https://github.com/anholt/libepoxy",
123+
"https://github.com/GNOME/libxml2",
124+
]
125+
}))
126+
);
127+
cy.get('#importCodemeta').click();
128+
129+
cy.get('#softwareRequirements').should('have.value', 'https://www.gtk.org\nhttps://github.com/anholt/libepoxy\nhttps://github.com/GNOME/libxml2');
130+
});
131+
132+
it('can be exported to a single URI', function() {
133+
cy.get('#name').type('My Test Software');
134+
135+
cy.get('#softwareRequirements').type('https://github.com/GNOME/libxml2');
136+
137+
cy.get('#generateCodemetaV2').click();
138+
139+
cy.get('#errorMessage').should('have.text', '');
140+
cy.get('#codemetaText').then((elem) => JSON.parse(elem.text()))
141+
.should('deep.equal', {
142+
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
143+
"type": "SoftwareSourceCode",
144+
"name": "My Test Software",
145+
"softwareRequirements": "https://github.com/GNOME/libxml2",
146+
});
147+
});
148+
149+
it('can be imported from a single URI', function() {
150+
cy.get('#codemetaText').then((elem) =>
151+
elem.text(JSON.stringify({
152+
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
153+
"@type": "SoftwareSourceCode",
154+
"name": "My Test Software",
155+
"softwareRequirements": "https://github.com/GNOME/libxml2",
156+
}))
157+
);
158+
cy.get('#importCodemeta').click();
159+
160+
cy.get('#softwareRequirements').should('have.value', 'https://github.com/GNOME/libxml2');
161+
});
162+
163+
it('cannot be exported as text', function() {
164+
cy.get('#name').type('My Test Software');
165+
166+
cy.get('#softwareRequirements').type('libxml2');
167+
168+
cy.get('#generateCodemetaV2').click();
169+
170+
cy.get('#errorMessage').should('have.text', 'Invalid URL in field "softwareRequirements": "libxml2"');
171+
cy.get('#codemetaText').then((elem) => JSON.parse(elem.text()))
172+
.should('deep.equal', {
173+
"@context": "https://doi.org/10.5063/schema/codemeta-2.0",
174+
"type": "SoftwareSourceCode",
175+
"name": "My Test Software",
176+
"softwareRequirements": [],
177+
});
178+
});
179+
});

js/codemeta_generation.js

+36-3
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,14 @@ const directCodemetaFields = [
9898
'referencePublication'
9999
];
100100

101+
// Tuples of codemeta property, character joining/splitting items in a textarea, and optionally
102+
// post-processing for deserialization and pre-processing for deserialization
101103
const splittedCodemetaFields = [
102104
['keywords', ','],
103105
['programmingLanguage', ','],
104106
['runtimePlatform', ','],
105107
['operatingSystem', ','],
106-
['softwareRequirements', '\n'],
108+
['softwareRequirements', '\n', generateUri, importUri],
107109
['relatedLink', '\n'],
108110
]
109111

@@ -136,6 +138,17 @@ function generateBlankNodeId(customId) {
136138
return `_:${customId}`;
137139
}
138140

141+
// Unambiguously converts a free-form text field that might be a URI or actual free text
142+
// in JSON-LD
143+
function generateUri(fieldName, text) {
144+
if (isUrl(text)) {
145+
return {"@id": text};
146+
} else {
147+
setError(`Invalid URL in field "${fieldName}": "${text}"`);
148+
return undefined;
149+
}
150+
}
151+
139152
function generateShortOrg(fieldName) {
140153
var affiliation = getIfSet(fieldName);
141154
if (affiliation !== undefined) {
@@ -246,9 +259,15 @@ async function buildExpandedDocWithAllContexts() {
246259
splittedCodemetaFields.forEach(function (item, index) {
247260
const id = item[0];
248261
const separator = item[1];
249-
const value = getIfSet('#' + id);
262+
const serializer = item[2];
263+
const deserializer = item[3];
264+
let value = getIfSet('#' + id);
250265
if (value !== undefined) {
251-
doc[id] = value.split(separator).map(trimSpaces);
266+
value = value.split(separator).map(trimSpaces);
267+
if (serializer !== undefined) {
268+
value = value.map((item) => serializer(id, item));
269+
}
270+
doc[id] = value;
252271
}
253272
});
254273

@@ -307,6 +326,11 @@ async function generateCodemeta(codemetaVersion = "2.0") {
307326
}
308327
}
309328

329+
// Imports a field that can be either URI or free-form text into a free-form text field
330+
function importUri(fieldName, doc) {
331+
return doc;
332+
}
333+
310334
// Imports a single field (name or @id) from an Organization.
311335
function importShortOrg(fieldName, doc) {
312336
if (doc !== undefined) {
@@ -433,10 +457,19 @@ async function importCodemeta() {
433457
splittedCodemetaFields.forEach(function (item, index) {
434458
const id = item[0];
435459
const separator = item[1];
460+
const serializer = item[2];
461+
const deserializer = item[3];
436462
let value = doc[id];
437463
if (value !== undefined) {
438464
if (Array.isArray(value)) {
465+
if (deserializer !== undefined) {
466+
value = value.map((item) => deserializer(id, item));
467+
}
439468
value = value.join(separator);
469+
} else {
470+
if (deserializer !== undefined) {
471+
value = deserializer(id, value);
472+
}
440473
}
441474
setIfDefined('#' + id, value);
442475
}

js/utils.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ function trimSpaces(s) {
2828
// From https://stackoverflow.com/a/43467144
2929
function isUrl(s) {
3030
try {
31-
new URL(s);
31+
const url = new URL(s);
32+
if (url.origin == "null") {
33+
// forbids "foo: bar" as a URL, for example
34+
return false;
35+
}
3236
return true;
3337
} catch (e) {
3438
return false;

js/validation/things.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ var softwareFieldValidators = {
217217
"processorRequirements": validateTexts,
218218
"releaseNotes": validateTextsOrUrls,
219219
"softwareHelp": validateCreativeWorks,
220-
"softwareRequirements": noValidation, // TODO: validate SoftwareSourceCode
220+
"softwareRequirements": validateUrls,
221221
"softwareVersion": validateText, // TODO?
222222
"storageRequirements": validateTextsOrUrls,
223223
"supportingData": noValidation, // TODO

0 commit comments

Comments
 (0)