Skip to content
Draft
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
17 changes: 17 additions & 0 deletions src/main/java/com/knowledgepixels/nanodash/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.util.Literals;
import org.eclipse.rdf4j.model.vocabulary.FOAF;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.XSD;
import org.nanopub.Nanopub;
import org.nanopub.NanopubUtils;
Expand Down Expand Up @@ -673,13 +675,28 @@ public static String sanitizeSvg(String rawSvg) {
/**
* Checks if a given string is likely to be HTML content.
*
* <p>This is a heuristic, needed wherever the RDF datatype is not available (query
* results arrive as flat strings). Where an actual literal is at hand, prefer
* {@link #isHtmlLiteral(Value)}, which relies on the declared datatype instead.
*
* @param value the string to check
* @return true if the given string is HTML content, false otherwise
*/
public static boolean looksLikeHtml(String value) {
return LEADING_TAG.matcher(value).find();
}

/**
* Checks whether a value is a literal explicitly tagged with the {@code rdf:HTML}
* datatype, i.e. whose content is meant to be rendered as HTML.
*
* @param value the value to check
* @return true if the value is an rdf:HTML literal, false otherwise
*/
public static boolean isHtmlLiteral(Value value) {
return value instanceof Literal literal && RDF.HTML.equals(literal.getDatatype());
}

public static boolean isDate(String value) {
return isDateLiteral(value) || isDateTimeLiteral(value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<body>

<wicket:panel>
"<span wicket:id="literal"></span>"
<span wicket:id="literal"></span>
</wicket:panel>

</body>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.knowledgepixels.nanodash.component;

import com.knowledgepixels.nanodash.Utils;
import com.knowledgepixels.nanodash.component.StatementItem.RepetitionGroup;
import com.knowledgepixels.nanodash.template.UnificationException;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.panel.Panel;
import org.eclipse.rdf4j.model.Literal;
Expand Down Expand Up @@ -31,7 +33,15 @@ public LiteralItem(String id, String parentId, Literal literal, RepetitionGroup
super(id);
this.literal = literal;
// this.context = rg.getContext();
add(new Label(LABEL_ID, literal.stringValue()));
if (Utils.isHtmlLiteral(literal)) {
// Content tagged as rdf:HTML is rendered as such, not shown as a quoted string
// (see issue #378).
add(new Label(LABEL_ID, Utils.sanitizeHtml(literal.stringValue()))
.setEscapeModelStrings(false)
.add(new AttributeAppender("class", "internal")));
} else {
add(new Label(LABEL_ID, "\"" + literal.stringValue() + "\""));
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.util.Literals;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.XSD;
import org.nanopub.Nanopub;
import org.nanopub.NanopubUtils;
Expand Down Expand Up @@ -473,18 +474,22 @@ public void unifyWith(Value v) throws UnificationException {
linkComp.add(AttributeAppender.append("class", "long-literal collapsed"));
showMoreLabelLiteral.setVisible(true);
}
boolean renderAsHtml = renderAsHtml(vL);
// An rdf:HTML literal gets no datatype marker: the rendered content itself shows
// what it is, just as dates are shown without an "(xsd:date)" suffix.
boolean showDatatype = !vL.getDatatype().equals(XSD.STRING) && !renderAsHtml;
if (vL.getLanguage().isPresent()) {
model.setObject("\"" + vs + "\"");
languageModel.setObject("(" + Literals.normalizeLanguageTag(vL.getLanguage().get()) + ")");
languageComp.setVisible(true);
} else if (!vL.getDatatype().equals(XSD.STRING)) {
} else if (showDatatype) {
model.setObject("\"" + vs + "\"");
datatypeModel.setObject("(" + vL.getDatatype().stringValue().replace(XSD.NAMESPACE, "xsd:") + ")");
datatypeComp.setVisible(true);
} else {
model.setObject("\"" + vs + "\"");
}
if (Utils.looksLikeHtml(vs)) {
if (renderAsHtml) {
linkComp.setVisible(false);
extraModel.setObject(Utils.sanitizeHtml(vs));
extraComp.setEscapeModelStrings(false);
Expand All @@ -495,6 +500,27 @@ public void unifyWith(Value v) throws UnificationException {
}
}

/**
* Decides whether a literal's content is to be rendered as HTML rather than escaped.
*
* <p>The datatype decides: {@code rdf:HTML} on the literal itself, or declared by the
* template for this placeholder, means HTML. Only when the template declares no
* datatype at all do we fall back to the pattern heuristic, which covers nanopubs
* published before HTML content was tagged, and those made without a template
* (see issue #378).
*
* @param literal the literal to be displayed
* @return true if the content is to be rendered as HTML
*/
private boolean renderAsHtml(Literal literal) {
if (Utils.isHtmlLiteral(literal)) return true;
// A language-tagged literal is an rdf:langString, so a datatype the template
// declares alongside the tag does not apply to it.
IRI declaredDatatype = literal.getLanguage().isPresent() ? null : template.getDatatype(iri);
if (declaredDatatype != null) return RDF.HTML.equals(declaredDatatype);
return Utils.looksLikeHtml(literal.stringValue());
}

/**
* Validator class for validating the input.
*/
Expand Down
60 changes: 43 additions & 17 deletions src/test/java/com/knowledgepixels/nanodash/UtilsTest.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,39 @@
package com.knowledgepixels.nanodash;

import com.knowledgepixels.nanodash.utils.TestUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.tester.WicketTester;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.util.Values;
import static org.eclipse.rdf4j.model.util.Values.iri;
import static org.eclipse.rdf4j.model.util.Values.literal;
import org.eclipse.rdf4j.model.vocabulary.FOAF;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.XSD;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.nanopub.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import org.nanopub.MalformedNanopubException;
import org.nanopub.Nanopub;
import org.nanopub.NanopubAlreadyFinalizedException;
import org.nanopub.NanopubCreator;
import org.nanopub.NanopubUtils;
import org.nanopub.vocabulary.FIP;
import org.nanopub.vocabulary.NPX;

import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.eclipse.rdf4j.model.util.Values.iri;
import static org.eclipse.rdf4j.model.util.Values.literal;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.knowledgepixels.nanodash.utils.TestUtils;

class UtilsTest {

Expand Down Expand Up @@ -137,6 +147,22 @@ void sanitizeHtmlPolicyPreservesAllowedProtocols() {
assertEquals("<a href=\"mailto:test&#64;knowledgepixels.com\" rel=\"nofollow\">Email</a>", sanitizedHtml);
}

@Test
void isHtmlLiteralRecognizesRdfHtmlDatatype() {
assertTrue(Utils.isHtmlLiteral(literal("<p>Hello</p>", RDF.HTML)));
assertTrue(Utils.isHtmlLiteral(literal("no tags at all", RDF.HTML)),
"the datatype decides, not the content");
}

@Test
void isHtmlLiteralRejectsOtherValues() {
assertFalse(Utils.isHtmlLiteral(literal("<p>Hello</p>")), "plain string literal");
assertFalse(Utils.isHtmlLiteral(literal("<p>Hello</p>", "en")), "language-tagged literal");
assertFalse(Utils.isHtmlLiteral(literal("42", XSD.INTEGER)), "other datatype");
assertFalse(Utils.isHtmlLiteral(iri("https://example.org/thing")), "IRI");
assertFalse(Utils.isHtmlLiteral(null), "null");
}

@Test
void sanitizeSvgKeepsStaticSvgSubset() {
String rawSvg = "<svg viewBox=\"0 0 504 900\" width=\"504\" height=\"900\" font-family=\"sans-serif\">"
Expand All @@ -149,10 +175,10 @@ void sanitizeSvgKeepsStaticSvgSubset() {
+ "<title>Registry (full label)</title></a>"
+ "</text></g></svg>";
String sanitized = Utils.sanitizeSvg(rawSvg);
for (String kept : new String[] {"<svg", "viewBox=\"0 0 504 900\"", "<rect", "<g", "transform=", "<line",
"<path", "d=\"M 0 0 L 10 10\"", "<text", "text-anchor=", "<tspan", "dy=\"15\"",
"href=\"https://w3id.org/fair/fip/terms/Registry\"", "Registry",
"<title>Registry (full label)</title>"}) {
for (String kept : new String[]{"<svg", "viewBox=\"0 0 504 900\"", "<rect", "<g", "transform=", "<line",
"<path", "d=\"M 0 0 L 10 10\"", "<text", "text-anchor=", "<tspan", "dy=\"15\"",
"href=\"https://w3id.org/fair/fip/terms/Registry\"", "Registry",
"<title>Registry (full label)</title>"}) {
assertTrue(sanitized.contains(kept), "expected to keep: " + kept + " in: " + sanitized);
}
}
Expand All @@ -178,8 +204,8 @@ void sanitizeSvgRemovesScriptingAndStyling() {
+ "<style>rect { fill: red; }</style>"
+ "</svg>";
String sanitized = Utils.sanitizeSvg(rawSvg);
for (String dropped : new String[] {"<script", "alert", "onclick", "onload", "style", "<foreignObject",
"<use", "<image", "evil.example"}) {
for (String dropped : new String[]{"<script", "alert", "onclick", "onload", "style", "<foreignObject",
"<use", "<image", "evil.example"}) {
assertFalse(sanitized.contains(dropped), "expected to drop: " + dropped + " but got: " + sanitized);
}
assertTrue(sanitized.contains("<rect width=\"10\" height=\"10\""));
Expand Down Expand Up @@ -1001,4 +1027,4 @@ void isDate_rejectsNonIsoShapes() {
assertFalse(Utils.isDate("2026-01-30 12:00:00")); // missing T
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.apache.wicket.util.tester.WicketTester;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand All @@ -29,6 +30,41 @@ void testLiteralItemRendersLiteral() {
tester.assertComponent("literalItem", LiteralItem.class);
}

@Test
void htmlLiteralIsRendered() {
Literal literal = TestUtils.vf.createLiteral("<p>Hello <em>world</em></p>", RDF.HTML);
LiteralItem item = new LiteralItem("literalItem", null, literal, null);

tester.startComponentInPage(item);
String html = tester.getLastResponseAsString();
assertTrue(html.contains("<p>Hello <em>world</em></p>"), html);
assertFalse(html.contains("&lt;p&gt;"), html);
}

@Test
void htmlLiteralIsSanitized() {
Literal literal = TestUtils.vf.createLiteral("<p onclick=\"alert('x')\">Hi</p><script>alert('x')</script>", RDF.HTML);
LiteralItem item = new LiteralItem("literalItem", null, literal, null);

tester.startComponentInPage(item);
String html = tester.getLastResponseAsString();
assertFalse(html.contains("onclick"), html);
assertFalse(html.contains("<script>"), html);
assertTrue(html.contains("<p>Hi</p>"), html);
}

@Test
void plainLiteralWithMarkupIsEscapedAndQuoted() {
Literal literal = TestUtils.vf.createLiteral("<p>not html</p>");
LiteralItem item = new LiteralItem("literalItem", null, literal, null);

tester.startComponentInPage(item);
String html = tester.getLastResponseAsString();
assertTrue(html.contains("&quot;&lt;p&gt;not html&lt;/p&gt;&quot;"),
"escaped, and still shown in quotes as any other literal: " + html);
assertFalse(html.contains("<p>not html</p>"), html);
}

@Test
void isUnifiableWithReturnsTrueForSameLiteralValue() {
Literal lit1 = TestUtils.vf.createLiteral("value");
Expand Down
Loading