diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/pom.xml b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/pom.xml
index 9fb16396afd4..25a370f91394 100644
--- a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/pom.xml
@@ -43,5 +43,18 @@
${project.version}
runtime
+
+
+ org.xwiki.platform
+ xwiki-platform-test-page
+ ${project.version}
+ test
+
+
+ org.xwiki.platform
+ xwiki-platform-web-templates
+ ${project.version}
+ test
+
diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/src/main/resources/XWiki/AttachmentSelector.xml b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/src/main/resources/XWiki/AttachmentSelector.xml
index fe50f4eb55cb..9173b9b1fa7d 100644
--- a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/src/main/resources/XWiki/AttachmentSelector.xml
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/src/main/resources/XWiki/AttachmentSelector.xml
@@ -117,7 +117,10 @@ $xwiki.jsx.use($attachmentPickerDocName)
#set ($returnURL = $escapetool.url($doc.getURL('view', $request.queryString)))
#set ($deleteURL = $targetAttachDocument.getAttachmentURL($attachment.filename, 'delattachment', "xredirect=${returnURL}&form_token=$!{services.csrf.getToken()}") )
#set ($viewURL = $targetAttachDocument.getAttachmentURL($attachment.filename) )##{'name' : 'download', 'url' : $viewURL, 'rel' : '__blank'}
- #set ($selectURL = $targetDocument.getURL(${options.get('docAction')}, "${options.get('classname')}_${options.get('object')}_${options.get('property')}=${attachment.filename}&form_token=$!{services.csrf.getToken()}"))
+ #set ($selectURL = $targetDocument.getURL(${options.get('docAction')}, $escapetool.url({
+ "${options.get('classname')}_${options.get('object')}_${options.get('property')}": ${attachment.filename},
+ 'form_token': $!{services.csrf.getToken()}
+ })))
#attachmentPicker_displayEndFrame ([{'name' : 'select', 'url' : $selectURL}, {'name' : 'delete', 'url' : $deleteURL}])
#end
@@ -130,7 +133,9 @@ $xwiki.jsx.use($attachmentPickerDocName)
*#
#macro (attachmentPicker_displayStartFrame $boxOptions $currentValue)
(% class="gallery_attachmentbox $!{boxOptions.cssClass} #if ("$!{boxOptions.value}" == $currentValue) current#{end}" %)(((
- (% class="gallery_attachmenttitle" title="$!{boxOptions.value}" %)((($boxOptions.text)))
+ (% class="gallery_attachmenttitle" title="$services.rendering.escape($!{boxOptions.value}, 'xwiki/2.1')" %)(((
+ $services.rendering.escape($boxOptions.text, 'xwiki/2.1')
+ )))
(% class="gallery_attachmentframe" %)(((
#end
@@ -146,13 +151,13 @@ $xwiki.jsx.use($attachmentPickerDocName)
## Compute the attachment reference because there's no getter.
#set ($attachmentReference = $services.model.createAttachmentReference($attachment.document.documentReference,
$attachment.filename))
- #set ($attachmentStringReference = $services.model.serialize($attachmentReference, 'default'))
+ #set ($attachmentStringReference = $services.rendering.escape($services.model.serialize($attachmentReference, 'default'), 'xwiki/2.1'))
#if ($attachment.isImage() && $options.displayImage)
## We add the version to the query string in order to invalidate the cache when an image attachment is replaced.
#set ($queryString = $escapetool.url({'version': $attachment.version}))
[[[[image:${attachmentStringReference}||width=180 queryString="$queryString"]]>>attach:$attachmentStringReference]]
#else
- * (% class="mime" %){{html wiki=false clean=false}}#mimetypeimg($attachment.getMimeType().toLowerCase() $attachment.getFilename().toLowerCase()){{/html}}(%%) (% class="filename" %)$attachment.getFilename()(% %)
+ * (% class="mime" %){{html wiki=false clean=false}}#mimetypeimg($attachment.getMimeType().toLowerCase() $attachment.getFilename().toLowerCase()){{/html}}(%%) (% class="filename" %)$services.rendering.escape($attachment.getFilename(), 'xwiki/2.1')(% %)
* v$attachment.getVersion() (#dynamicsize($attachment.longSize))
* $services.localization.render('core.viewers.attachments.author', [$!{xwiki.getUserName($attachment.author, false)}]) $services.localization.render('core.viewers.attachments.date', [$!{xwiki.formatDate($attachment.date, 'dd/MM/yyyy hh:mm')}])
* (% class="buttonwrapper" %)[[${services.localization.render("${translationPrefix}.actions.download")}>>attach:${attachmentStringReference}||title="$services.localization.render("${translationPrefix}.actions.download")" rel="__blank" class="button"]](%%)
@@ -1426,9 +1431,18 @@ $xwiki.ssx.use($xcontext.macro.doc.fullName)##
#set ($displayImage = false)
#end
#if ($displayImage)
- #set ($alt = "$!{xcontext.macro.params.alternateText}")
- #set ($width = "$!{xcontext.macro.params.width}")
- #set ($height = "$!{xcontext.macro.params.height}")
+ #set ($alt = '')
+ #set ($width = '')
+ #set ($height = '')
+ #if ($xcontext.macro.params.alternateText)
+ #set ($alt = "$services.rendering.escape($!{xcontext.macro.params.alternateText}, 'xwiki/2.1')")
+ #end
+ #if ($xcontext.macro.params.width)
+ #set ($width = "$services.rendering.escape($!{xcontext.macro.params.width}, 'xwiki/2.1')")
+ #end
+ #if ($xcontext.macro.params.height)
+ #set ($height = "$services.rendering.escape($!{xcontext.macro.params.height}, 'xwiki/2.1')")
+ #end
#set ($imageParams = '')
#if ("${width}" != '')
#set($imageParams = "$!{imageParams} width=${width}")
@@ -1493,9 +1507,9 @@ $xwiki.ssx.use($xcontext.macro.doc.fullName)##
#set ($attachmentResource = '')
#end
#if ($displayImage)
- (% class="$!{cssClass}#if (!$attachment) hidden#end" %)(((#if ("$!{attachmentResource}" != '' || $forceElement)#if($withLink)[[#end[[image:${attachmentResource}$!{imageParams}]]#if($withLink)>>attach:${attachmentResource}||rel=lightbox]]#{end}#end)))##
+ (% class="$!{cssClass}#if (!$attachment) hidden#end" %)(((#if ("$!{attachmentResource}" != '' || $forceElement)#if($withLink)[[#end[[image:$services.rendering.escape(${attachmentResource}, 'xwiki/2.1')$!{imageParams}]]#if($withLink)>>attach:$services.rendering.escape(${attachmentResource},'xwiki/2.1')||rel=lightbox]]#{end}#end)))##
#else
- (% class="$!{cssClass}" %)#if ("$!{attachmentResource}" != '' || $forceElement)#if ($withLink)[[attach:${attachmentResource}||rel=__blank]]#{else}(% class="displayed" %)#if($targetPermView)$!{attachmentName}#{else}Access Denied#{end}(% %)#{end}#end(%%)##
+ (% class="$!{cssClass}" %)#if ("$!{attachmentResource}" != '' || $forceElement)#if ($withLink)[[attach:${attachmentResource}||rel=__blank]]#{else}(% class="displayed" %)#if($targetPermView)$services.rendering.escape($!{attachmentName}, 'xwiki/2.1')#{else}Access Denied#{end}(% %)#{end}#end(%%)##
#end
#end
@@ -1518,8 +1532,9 @@ $xwiki.ssx.use($xcontext.macro.doc.fullName)##
#if ($hasTargetDoc)
#set ($queryString.targetdocname = $targetdoc.fullName)
#end
- (% class="buttonwrapper" %)[[$buttontext>>${xcontext.macro.doc.fullName}||queryString="$escapetool.url($queryString)"
- class="attachment-picker-start button" title="$buttontext"]](%%)##
+ #set ($linkLabel = $services.rendering.escape($services.rendering.escape($buttontext, 'xwiki/2.1'), 'xwiki/2.1'))
+ (% class="buttonwrapper" %)[[$linkLabel>>${xcontext.macro.doc.fullName}||queryString="$escapetool.url($queryString)"
+ class="attachment-picker-start button" title="$services.rendering.escape($buttontext, 'xwiki/2.1')"]](%%)##
#end
#end
{{/velocity}}
diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/src/test/java/org/xwiki/attachment/AttachmentSelectorPageTest.java b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/src/test/java/org/xwiki/attachment/AttachmentSelectorPageTest.java
new file mode 100644
index 000000000000..b31399ee7652
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/src/test/java/org/xwiki/attachment/AttachmentSelectorPageTest.java
@@ -0,0 +1,330 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.attachment;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.stream.Stream;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.velocity.tools.generic.MathTool;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.xwiki.bridge.event.DocumentCreatedEvent;
+import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.component.wiki.internal.bridge.DefaultContentParser;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.script.ModelScriptService;
+import org.xwiki.observation.EventListener;
+import org.xwiki.rendering.internal.configuration.DefaultExtendedRenderingConfiguration;
+import org.xwiki.rendering.internal.configuration.RenderingConfigClassDocumentConfigurationSource;
+import org.xwiki.rendering.internal.macro.wikibridge.DefaultWikiMacroManager;
+import org.xwiki.rendering.internal.macro.wikibridge.WikiMacroEventListener;
+import org.xwiki.rendering.internal.syntax.SyntaxConverter;
+import org.xwiki.rendering.script.RenderingScriptService;
+import org.xwiki.rendering.syntax.Syntax;
+import org.xwiki.rendering.wikimacro.internal.DefaultWikiMacroFactory;
+import org.xwiki.rendering.wikimacro.internal.DefaultWikiMacroRenderer;
+import org.xwiki.security.authorization.AuthorizationManager;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.test.annotation.ComponentList;
+import org.xwiki.test.page.HTML50ComponentList;
+import org.xwiki.test.page.PageTest;
+import org.xwiki.test.page.XWikiSyntax21ComponentList;
+import org.xwiki.velocity.tools.CollectionTool;
+import org.xwiki.xml.internal.html.filter.ControlCharactersFilter;
+
+import com.xpn.xwiki.XWikiException;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.BaseObject;
+import com.xpn.xwiki.objects.classes.BaseClass;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test of {@code XWiki.AttachmentSelector}.
+ *
+ * @version $Id$
+ * @since 14.5RC1
+ * @since 14.4.2
+ * @since 13.10.7
+ */
+@HTML50ComponentList
+@XWikiSyntax21ComponentList
+@ComponentList({
+ // Start RenderingScriptService
+ RenderingScriptService.class,
+ DefaultExtendedRenderingConfiguration.class,
+ RenderingConfigClassDocumentConfigurationSource.class,
+ SyntaxConverter.class,
+ // End RenderingScriptService
+ ControlCharactersFilter.class,
+ ModelScriptService.class,
+ TestNoScriptMacro.class,
+ // Start WikiMacroEventListener
+ WikiMacroEventListener.class,
+ DefaultWikiMacroFactory.class,
+ DefaultWikiMacroManager.class,
+ DefaultContentParser.class,
+ org.xwiki.rendering.internal.parser.DefaultContentParser.class,
+ DefaultWikiMacroRenderer.class
+ // End WikiMacroEventListener
+})
+class AttachmentSelectorPageTest extends PageTest
+{
+ @BeforeEach
+ void setUp() throws Exception
+ {
+ registerVelocityTool("collectiontool", new CollectionTool());
+ registerVelocityTool("mathtool", new MathTool());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "{{noscript}}println(\"Hello from noscript!\"){{/noscript}}.png",
+ "]] {{noscript}}println(\"Hello from noscript!\"){{/noscript}}.png"
+ })
+ void renderDisplayImageFalse(String fileName) throws Exception
+ {
+ commonFixup(fileName);
+
+ this.request.put("docname", "xwiki:Space.Test");
+ this.request.put("classname", "xwiki:Space.Test");
+ this.request.put("property", "avatar");
+ this.request.put("object", "0");
+ this.request.put("filter", "png");
+ this.request.put("displayImage", "false");
+ this.request.put("xpage", "plain");
+
+ Document document = renderHTMLPage(new DocumentReference("xwiki", "XWiki", "AttachmentSelector"));
+
+ assertNotNull(document);
+ Element galleryAttachmentTitle = document.select(".gallery_attachmenttitle").get(1);
+ assertEquals(fileName, galleryAttachmentTitle.attr("title"));
+ assertEquals(fileName, galleryAttachmentTitle.text());
+ assertEquals(fileName, document.select(".gallery_attachmentframe .filename").text());
+ assertEquals(String.format("attach:xwiki:Space.Test@%s", fileName),
+ document.select(".gallery_attachmentframe a").attr("href"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "{{noscript}}println(\"Hello from noscript!\"){{/noscript}}.png",
+ "]] {{noscript}}println(\"Hello from noscript!\"){{/noscript}}.png"
+ })
+ void renderDisplayImageTrue(String fileName) throws Exception
+ {
+ commonFixup(fileName);
+
+ this.request.put("docname", "xwiki:Space.Test");
+ this.request.put("classname", "xwiki:Space.Test");
+ this.request.put("property", "avatar");
+ this.request.put("object", "0");
+ this.request.put("filter", "png");
+ this.request.put("displayImage", "true");
+ this.request.put("xpage", "plain");
+
+ Document document = renderHTMLPage(new DocumentReference("xwiki", "XWiki", "AttachmentSelector"));
+ assertNotNull(document);
+ Element galleryAttachmentTitle = document.select(".gallery_attachmenttitle").get(1);
+ assertEquals(fileName, galleryAttachmentTitle.attr("title"));
+ assertEquals(fileName, galleryAttachmentTitle.text());
+ assertEquals(String.format("attach:xwiki:Space.Test@%s", fileName),
+ document.select(".gallery_attachmentframe a").attr("href"));
+
+ Elements img = document.select(".gallery_attachmentframe img");
+ assertEquals(String.format("xwiki:Space.Test@%s", fileName), img.attr("src"));
+ assertEquals(String.format("xwiki:Space.Test@%s", fileName), img.attr("alt"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("attachmentSelectorMacroSource")
+ void attachmentSelectorMacroWidth(String widthValue, String expectedWidth) throws Exception
+ {
+ attachmentSelectorMacroFixup();
+
+ XWikiDocument xwikiDocument = commonFixup("test.png");
+
+ xwikiDocument.setContent(String.format("{{attachmentSelector "
+ + "classname=\"Space.Test\" "
+ + "property=\"avatar\" "
+ + "savemode=\"direct\" "
+ + "width=\"%s\" "
+ + "displayImage=\"true\"/}}", widthValue));
+
+ Document document = renderHTMLPage(xwikiDocument);
+ assertEquals(expectedWidth, document.select(".displayed img").attr("width"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("attachmentSelectorMacroSource")
+ void attachmentSelectorMacroHeight(String widthValue, String expectedWidth) throws Exception
+ {
+ attachmentSelectorMacroFixup();
+
+ XWikiDocument xwikiDocument = commonFixup("test.png");
+
+ xwikiDocument.setContent(String.format("{{attachmentSelector "
+ + "classname=\"Space.Test\" "
+ + "property=\"avatar\" "
+ + "savemode=\"direct\" "
+ + "height=\"%s\" "
+ + "displayImage=\"true\"/}}", widthValue));
+ Document document = renderHTMLPage(xwikiDocument);
+ assertEquals(expectedWidth, document.select(".displayed img").attr("height"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("attachmentSelectorMacroSource")
+ void attachmentSelectorMacroAlternateText(String widthValue, String expectedWidth) throws Exception
+ {
+ attachmentSelectorMacroFixup();
+
+ XWikiDocument xwikiDocument = commonFixup("test.png");
+
+ xwikiDocument.setContent(String.format("{{attachmentSelector "
+ + "classname=\"Space.Test\" "
+ + "property=\"avatar\" "
+ + "savemode=\"direct\" "
+ + "alternateText=\"%s\" "
+ + "displayImage=\"true\"/}}", widthValue));
+ xwikiDocument.setSyntax(Syntax.XWIKI_2_1);
+ Document document = renderHTMLPage(xwikiDocument);
+ assertEquals(expectedWidth, document.select(".displayed img").attr("alt"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("attachmentSelectorMacroSource")
+ void attachmentSelectorMacroWidthWithLink(String widthValue, String expectedWidth) throws Exception
+ {
+ attachmentSelectorMacroFixup();
+
+ XWikiDocument xwikiDocument = commonFixup("test.png");
+
+ xwikiDocument.setContent(String.format("{{attachmentSelector "
+ + "classname=\"Space.Test\" "
+ + "property=\"avatar\" "
+ + "savemode=\"direct\" "
+ + "width=\"%s\" "
+ + "link=\"true\" "
+ + "displayImage=\"true\"/}}", widthValue));
+
+ Document document = renderHTMLPage(xwikiDocument);
+ assertEquals(expectedWidth, document.select(".displayed img").attr("width"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("attachmentSelectorMacroSource")
+ void attachmentSelectorMacroHeightWithLink(String widthValue, String expectedWidth) throws Exception
+ {
+ attachmentSelectorMacroFixup();
+
+ XWikiDocument xwikiDocument = commonFixup("test.png");
+
+ xwikiDocument.setContent(String.format("{{attachmentSelector "
+ + "classname=\"Space.Test\" "
+ + "property=\"avatar\" "
+ + "savemode=\"direct\" "
+ + "height=\"%s\" "
+ + "link=\"true\" "
+ + "displayImage=\"true\"/}}", widthValue));
+ Document document = renderHTMLPage(xwikiDocument);
+ assertEquals(expectedWidth, document.select(".displayed img").attr("height"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("attachmentSelectorMacroSource")
+ void attachmentSelectorMacroAlternateTextWithLink(String widthValue, String expectedWidth) throws Exception
+ {
+ attachmentSelectorMacroFixup();
+
+ XWikiDocument xwikiDocument = commonFixup("test.png");
+
+ xwikiDocument.setContent(String.format("{{attachmentSelector "
+ + "classname=\"Space.Test\" "
+ + "property=\"avatar\" "
+ + "savemode=\"direct\" "
+ + "alternateText=\"%s\" "
+ + "link=\"true\" "
+ + "displayImage=\"true\"/}}", widthValue));
+ xwikiDocument.setSyntax(Syntax.XWIKI_2_1);
+ Document document = renderHTMLPage(xwikiDocument);
+ assertEquals(expectedWidth, document.select(".displayed img").attr("alt"));
+ }
+
+ private void attachmentSelectorMacroFixup() throws Exception
+ {
+ DocumentReference attachmentSelectorDocumentReference =
+ new DocumentReference("xwiki", "XWiki", "AttachmentSelector");
+ XWikiDocument xWikiDocument = loadPage(attachmentSelectorDocumentReference);
+
+ // TODO: The code below is more generic than this test and should be moved to be reusable.
+ // Make the wiki component manager point to the default component manager.
+ this.componentManager.registerComponent(ComponentManager.class, "wiki",
+ this.componentManager.getInstance(ComponentManager.class));
+ when(this.componentManager.getInstance(AuthorizationManager.class)
+ .hasAccess(Right.ADMIN, new DocumentReference("xwiki", "XWiki", "Admin"),
+ xWikiDocument.getDocumentReference().getWikiReference())).thenReturn(true);
+ // Simulate the event not thrown by page test for now.
+ this.componentManager.getInstance(EventListener.class, "wikimacrolistener")
+ .onEvent(new DocumentCreatedEvent(), xWikiDocument, null);
+ }
+
+ private XWikiDocument commonFixup(String fileName) throws XWikiException, IOException
+ {
+ // Initialize a document containing an XClass definition and an XObject of this XClass.
+ // The interesting property is the avatar string field, which references the name of an attachment contained in
+ // the document itself too.
+ DocumentReference documentReference = new DocumentReference("xwiki", "Space", "Test");
+ XWikiDocument xwikiDocument = this.xwiki.getDocument(documentReference, this.context);
+ BaseClass xClass = xwikiDocument.getXClass();
+ xClass.addTextField("avatar", "Avatar", 10);
+ BaseObject baseObject = xwikiDocument.newXObject(xwikiDocument.getDocumentReference(), this.context);
+ baseObject.setStringValue("avatar", fileName);
+ // Simulates the file rename action.
+ xwikiDocument.setAttachment("tmp.png", IOUtils.toInputStream("", Charset.defaultCharset()), this.context);
+ xwikiDocument.getAttachment("tmp.png").setFilename(fileName);
+ xwikiDocument.setSyntax(Syntax.XWIKI_2_1);
+
+ this.xwiki.saveDocument(xwikiDocument, this.context);
+
+ this.context.setDoc(xwikiDocument);
+
+ return xwikiDocument;
+ }
+
+ public static Stream attachmentSelectorMacroSource()
+ {
+ return Stream.of(
+ arguments("{{noscript /~}~}", "{{noscript /}}"),
+ arguments("]] {{noscript /~}~}", "]] {{noscript /}}"),
+ arguments("]] {{noscript /~}~} [[", "]] {{noscript /}} [[")
+ );
+ }
+}
diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/src/test/java/org/xwiki/attachment/TestNoScriptMacro.java b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/src/test/java/org/xwiki/attachment/TestNoScriptMacro.java
new file mode 100644
index 000000000000..a0cf70874af8
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-ui/src/test/java/org/xwiki/attachment/TestNoScriptMacro.java
@@ -0,0 +1,72 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.attachment;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.rendering.block.Block;
+import org.xwiki.rendering.macro.AbstractMacro;
+import org.xwiki.rendering.transformation.MacroTransformationContext;
+
+import static java.util.Collections.emptyList;
+
+/**
+ * This script prints an error log when it is interpreted, making the test fail.
+ *
+ * @version $Id$
+ * @since 14.5RC1
+ * @since 14.4.2
+ * @since 13.10.7
+ */
+@Component
+@Named("noscript")
+@Singleton
+public class TestNoScriptMacro extends AbstractMacro