CVE-2022-36097: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: XWiki Platform Attachment UI provides a macro to easily upload and select attachments for XWiki Platform, a generic wiki platform. Starting with version 14.0-rc-1 and prior to 14.4-rc-1, it's possible to store JavaScript in an attachment name, which will be executed by anyone trying to move the corresponding attachment. This issue has been patched in XWiki 14.4-rc-1. As a workaround, one may copy `moveStep1.vm` to `webapp/xwiki/templates/moveStep1.vm` and replace vulnerable code with code from the patch.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2022-09-08T21:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Other
Files changed: xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/ApplicationResources.properties, xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/move.vm, xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStatus.vm, xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStep1.vm, xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/refactoring_macros.vm, xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MovePageTest.java, xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MoveStatusPagesTest.java, xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MoveStep1PageTest.java, xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/RefactoringMacrosPageTest.java
Commit date: 2022-05-05 09:52:21 +0200
Commit message: XWIKI-19667: The move attachment form is missing escaping and some translations

Diff: diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/ApplicationResources.properties b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/ApplicationResources.properties
index 540409a02146..c835e32361e5 100644
--- a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/ApplicationResources.properties
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/ApplicationResources.properties
@@ -35,10 +35,13 @@ attachment.move.newName.placeholder=NewAttachmentName
 attachment.move.targetNotWritable=You don't have the right to create or edit the target page.
 attachment.move.submit=Submit
 attachment.move.status.notFound=The requested attachment move status could not be found.
 attachment.move.status.label=Attachment Move Status
 attachment.move.status.hint=The following attachment move operation has been started by {0} on {1}.
+attachment.move.targetLocation.modal.title=Select New Attachment Location
+attachment.move.targetLocation.modal.selectButton=Select
+attachment.move.targetLocation.modal.closeButton=Close
 attachment.job.saveDocument.inPlace=Attachment {0} moved to {1}.
 attachment.job.saveDocument.source=Attachment moved to {0}.
 attachment.job.saveDocument.target=Attachment moved from {0}.
 attachment.job.rollbackDocument.target=Rollback because moving [{0}] to [{1}] failed.
 attachment.job.creation.error=Failed to create a move attachment job.
diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/move.vm b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/move.vm
index 1b32b5ad8c48..7d71fa34d19f 100644
--- a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/move.vm
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/move.vm
@@ -61,11 +61,11 @@
       #set ($autoRedirect = $request.autoRedirect == 'true')
       #set ($updateReferences = $request.updateReferences == 'true')
       ## Do the move. It's the form in attachment/moveStep1.vm page that calls this page with step=2.
       #if ("$!targetAttachmentName.trim()" == '' || "$!targetLocation.trim()" == '')
         $response.setStatus(400)
-        #error($services.localization.render('attachment.move.emptyName'))
+        #error($escapetool.xml($services.localization.render('attachment.move.emptyName')))
         #template("attachment/moveStep1.vm")
       #else
         #template('attachment/refactoring_macros.vm')
         #if (!$services.attachment.locationAvailable($targetLocation, $targetAttachmentName)) 
           $response.setStatus(409)
@@ -73,21 +73,20 @@
             ${escapetool.xml($targetAttachmentName)}, ${escapetool.xml($targetLocation)}, 
             $xwiki.getURL($targetLocation)]))
           #template("attachment/moveStep1.vm")
         #elseif (!$services.security.authorization.hasAccess('edit', $targetLocation))
           $response.setStatus(403)
-          #error($services.localization.render('attachment.move.targetNotWritable',
-            [${escapetool.xml($targetLocation)}]))
+          #error($escapetool.xml($services.localization.render('attachment.move.targetNotWritable')))
           #template('attachment/moveStep1.vm')
         #else
           #try("moveJobException")
             #set ($moveRequest = $services.attachment.createMoveRequest($sourceLocation, $sourceAttachmentName, 
               $targetLocation, $targetAttachmentName, $xcontext.user, $autoRedirect, $updateReferences))
             #set ($moveJob = $services.attachment.createMoveJob($moveRequest))
           #end
           #if ("$!moveJobException" != '')
-            #displayException($services.localization.render('attachment.job.creation.error'), $moveJobException)
+            #displayException($escapetool.xml($services.localization.render('attachment.job.creation.error')), $moveJobException)
           #else
             $response.sendRedirect($doc.getURL($xcontext.action, $escapetool.url({
               'xpage': 'attachment/move',
               'moveId': $moveRequest.id.get($mathtool.sub($moveRequest.id.size(), 1))
             })))
diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStatus.vm b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStatus.vm
index 07b0ce8f899c..867e8ed7dda8 100644
--- a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStatus.vm
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStatus.vm
@@ -38,13 +38,15 @@
 #end
 
 #macro (displayMoveJobStatus $moveJobStatus)
   #set ($moveJobRequest = $moveJobStatus.request)
 <div class="xcontent">
-  <h2>$services.localization.render('attachment.move.status.label')</h2>
-  <p class="text-muted small">$services.localization.render('attachment.move.status.hint',
-  [$xwiki.getUserName($moveJobRequest.userReference), $xwiki.formatDate($moveJobStatus.startDate)])</p>
+  <h2>$escapetool.xml($services.localization.render('attachment.move.status.label'))</h2>
+  <p class="text-muted small">
+    $escapetool.xml($services.localization.render('attachment.move.status.hint', 
+      [$xwiki.getUserName($moveJobRequest.userReference), $xwiki.formatDate($moveJobStatus.startDate)]))
+  </p>
   #displayMoveJobRequest($moveJobRequest)
   #template('job_macros.vm')
   #displayJobStatus($moveJobStatus)
 </div>
 #end
@@ -56,15 +58,15 @@
 #end
 
 #set ($moveJobStatus = $services.job.getJobStatus(['refactoring', 'moveAttachment', $request.moveId]))
 #if ($moveJobStatus)
   #if ($xcontext.action == 'get')
-    #outputMoveJobStatusJSON($moveJobStatus)
+    #outputmoveJobStatusJSON($moveJobStatus)
   #else
     #displayMoveJobStatus($moveJobStatus)
   #end
 #else
   $response.setStatus(404)
 <div class="box errormessage">
-  $services.localization.render('attachment.move.status.notFound')
+  $escapetool.xml($services.localization.render('attachment.move.status.notFound'))
 </div>
 #end
\ No newline at end of file
diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStep1.vm b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStep1.vm
index d82098ea5be8..e11c813b8572 100644
--- a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStep1.vm
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/moveStep1.vm
@@ -21,12 +21,15 @@
 ### Move attachment template, step 1: choose new name and a new location + updated backlinks + redirection.
 ###
 
 #set ($discard = $xwiki.ssrx.use('css/attachment/move.css'))
 #set ($discard = $xwiki.jsrx.use('js/attachment/move.js'))
-#set($titleToDisplay = $services.localization.render('attachment.move.title', 
-  [$attachment.name, $escapetool.xml($doc.plainTitle), $doc.getURL()]))
+#set($titleToDisplay = $services.localization.render('attachment.move.title', [
+  $escapetool.xml($attachment.name), 
+  $escapetool.xml($doc.plainTitle), 
+  $escapetool.xml($doc.getURL())
+]))
 <div class="xcontent">
 #template('contentheader.vm')
 #template('attachment/refactoring_macros.vm')
 #template('refactoring_macros.vm')
 #template('locationPicker_macros.vm')
@@ -61,43 +64,56 @@
         ## - We don't have management UIs for redirects FTM and reducing the number of redirects make the wiki easier
         ##   to manage.
         ## In the future we'll offer a config option to define the default behavior, see
         ## http://jira.xwiki.org/browse/XWIKI-13384
         #set ($checked = $request.autoRedirect == 'true')
-        <dt>
+        <dt class="autoRedirect">
           <label>
             <input type="checkbox" name="autoRedirect" value="true" #if ($checked)checked="checked"#end />
-            $services.localization.render('attachment.move.autoRedirect.label')
+            $escapetool.xml($services.localization.render('attachment.move.autoRedirect.label'))
           </label>
           ## The value submitted when the checkbox is not checked, used to preserve the form state.
           <input type="hidden" name="autoRedirect" value="false" />
         </dt>
-        <dd>
-          <span class="xHint">$services.localization.render('attachment.move.autoRedirect.hint')</span>
+        <dd class="autoRedirect">
+          <span class="xHint">
+            $escapetool.xml($services.localization.render('attachment.move.autoRedirect.hint'))
+          </span>
         </dd>
       </dl>
     </div>
     <div class="col-xs-12 col-lg-6">
       <dl>
         ##------------
         ## Target attachment
         ##------------
-        <dt>
-          <label for="targetAttachmentName">$services.localization.render('attachment.move.newName.label')</label>
-          <span class="xHint">$!services.localization.render('attachment.move.newName.hint')</span>
+        <dt class="targetAttachmentName">
+          <label for="targetAttachmentNameTitle">
+            $escapetool.xml($services.localization.render('attachment.move.newName.label'))
+          </label>
+          <span class="xHint">
+            $escapetool.xml($services.localization.render('attachment.move.newName.hint'))
+          </span>
         </dt>
         <dd>
-          <input type="text" id="targetAttachmentNameTitle" name="targetAttachmentName" value="$escapetool.xml($attachment.name)"
+          <input type="text" 
+                 id="targetAttachmentNameTitle" 
+                 name="targetAttachmentName" 
+                 value="$escapetool.xml($attachment.name)"
                  class="location-title-field" 
-                 placeholder="$!services.localization.render('attachment.move.newName.placeholder')" /> 
+                 placeholder="$escapetool.xml($services.localization.render('attachment.move.newName.placeholder'))" /> 
         </dd>
         ##------------
         ## Target Page
         ##------------
         <dt>
-          <label for="$!{options.id}Title">$services.localization.render('attachment.move.newLocation.label')</label>
-          <span class="xHint">$!services.localization.render('attachment.move.newLocation.hint')</span>
+          <label for="targetLocation">
+              $escapetool.xml($services.localization.render('attachment.move.newLocation.label'))
+          </label>
+          <span class="xHint">
+            $escapetool.xml($services.localization.render('attachment.move.newLocation.hint'))
+          </span>
         </dt>
         <dd>
           #set ($targetRef = $doc.documentReference)
           #if ($request.targetLocation)
             #set ($targetRef = $request.targetLocation)
@@ -108,11 +124,13 @@
               <div class="modal-content">
                 <div class="modal-header">
                   <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                     <span aria-hidden="true">&times;</span>
                   </button>
-                  <h5 class="modal-title">Modal title</h5>
+                  <h5 class="modal-title">
+                    $escapetool.xml($services.localization.render('attachment.move.targetLocation.modal.title'))
+                  </h5>
                 </div>
                 <div class="modal-body">
                   #template("documentTree_macros.vm")
                   #documentTree({
                     'class': 'location-tree',
@@ -122,12 +140,16 @@
                     'showTranslations': false,
                     'showWikis': true
                   })
                 </div>
                 <div class="modal-footer">
-                  <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
-                  <button type="button" class="btn btn-primary">Select</button>
+                  <button type="button" class="btn btn-default" data-dismiss="modal">
+                    $escapetool.xml($services.localization.render('attachment.move.targetLocation.modal.closeButton'))
+                  </button>
+                  <button type="button" class="btn btn-primary">
+                    $escapetool.xml($services.localization.render('attachment.move.targetLocation.modal.selectButton'))
+                  </button>
                 </div>
               </div>
             </div>
           </div>
           <div class="input-group">
@@ -148,13 +170,17 @@
       </dl>
     </div>
   </div>
   <div class="buttons">
     <span class="buttonwrapper">
-      <input type="submit" value="$services.localization.render('attachment.move.submit')" class="button" />
+      <input type="submit" 
+             value="$escapetool.xml($services.localization.render('attachment.move.submit'))" 
+             class="button" />
     </span>
     <span class="buttonwrapper">
-      <a class="secondary button" href="$doc.getURL()">$services.localization.render('cancel')</a>
+      <a class="secondary button" href="$doc.getURL()">
+        $escapetool.xml($services.localization.render('cancel'))
+      </a>
     </span>
   </div>
 </form>
 </div>
diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/refactoring_macros.vm b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/refactoring_macros.vm
index 6f9e83b84fc2..653eea83c4a3 100644
--- a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/refactoring_macros.vm
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/resources/templates/attachment/refactoring_macros.vm
@@ -20,12 +20,16 @@
 ###
 ### Refactoring attachments macros.
 ###
 #macro (displaySourceAttachment) 
 <dt>
-  <label>$services.localization.render('attachment.move.source.label')</label>
-  <span class="xHint">$services.localization.render('attachment.move.source.hint')</span>
+  <label>
+    $escapetool.xml($services.localization.render('attachment.move.source.label'))
+  </label>
+  <span class="xHint">
+    $escapetool.xml($services.localization.render('attachment.move.source.hint'))
+  </span>
 </dt>
 <dd>
   #hierarchy($attachment)
   <input type="hidden" name="sourceLocation" 
           value="$!escapetool.xml($services.model.serialize($doc.documentReference, 'default'))" />
@@ -39,18 +43,18 @@
   ## (i.e. we want to make sure the links are updated for simple users).
 <dt#if ($hidden) class="hidden"#end>
   <label>
     #set ($checked = !$request.updateReferences || $request.updateReferences == 'true')
     <input type="checkbox" name="updateReferences" value="true" #if ($checked)checked="checked"#end />
-    $services.localization.render('attachment.move.links.label')
+    $escapetool.xml($services.localization.render('attachment.move.links.label'))
   </label>
   ## The value submitted when the checkbox is not checked, used to preserve the form state.
   <input type="hidden" name="updateReferences" value="false"/>
 </dt>
 <dd#if ($hidden) class="hidden"#end>
   #set ($backLinksCount = $services.attachment.backlinksCount($attachment))
   #set ($backLinksURL = '')
   <span class="xHint">
-    $services.localization.render('attachment.move.links.hint', [$backLinksCount])
+  $escapetool.xml($services.localization.render('attachment.move.links.hint', [$backLinksCount]))
   </span>
 </dd>
 #end
diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MovePageTest.java b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MovePageTest.java
index fb870812473f..1d0fb3853130 100644
--- a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MovePageTest.java
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MovePageTest.java
@@ -149,19 +149,29 @@ void submitMoveTargetEditNotAllowed() throws Exception
         this.request.put("targetAttachmentName", ATTACHMENT_NAME);
         this.request.put("targetLocation", "Space.Target");
         this.request.put("step", "2");
 
         Document render = Jsoup.parse(this.templateManager.render(MOVE_TEMPLATE));
-        assertEquals("error: attachment.move.targetNotWritable [Space.Target]",
+        assertEquals("error: attachment.move.targetNotWritable",
             render.getElementsByClass("errormessage").get(0).text());
     }
 
+    @Test
+    void submitTargetAttachmentNameEmpty() throws Exception
+    {
+        this.context.setDoc(this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "Source"), this.context));
+        this.request.put("step", "2");
+        this.request.put("form_token", "a6DSv7pKWcPargoTvyx2Ww");
+        Document render = Jsoup.parse(this.templateManager.render(MOVE_TEMPLATE));
+        assertEquals("error: attachment.move.emptyName", render.select(".errormessage").text());
+    }
+
     @Test
     void submitMoveTargetAlreadyExists() throws Exception
     {
         DocumentReference sourceDocumentReference = new DocumentReference("xwiki", "Space", "Source");
-        DocumentReference targetDocumentReference = new DocumentReference("xwiki", "Space", "Target");
+        DocumentReference targetDocumentReference = new DocumentReference("xwiki", "Space", "Target\"'");
         this.context.setDoc(this.xwiki.getDocument(sourceDocumentReference, this.context));
 
         // Allow the user to edit the source and target documents.
         when(this.contextualAuthorizationManager.hasAccess(Right.EDIT, sourceDocumentReference)).thenReturn(true);
         when(this.contextualAuthorizationManager.hasAccess(Right.EDIT, targetDocumentReference)).thenReturn(true);
@@ -176,14 +186,14 @@ void submitMoveTargetAlreadyExists() throws Exception
         this.request.put("sourceSpaceName", "Space");
         this.request.put("sourcePageName", "Source");
         this.request.put("sourceAttachmentName", "oldname.txt");
         this.request.put("autoRedirect", "true");
         this.request.put("targetAttachmentName", ATTACHMENT_NAME);
-        this.request.put("targetLocation", "Space.Target");
+        this.request.put("targetLocation", "Space.Target\"'");
         this.request.put("step", "2");
 
         Document render = Jsoup.parse(this.templateManager.render(MOVE_TEMPLATE));
-        assertEquals(
-            "error: attachment.move.alreadyExists [attachment.txt, Space.Target, /xwiki/bin/view/Space/Target]",
+        assertEquals("error: attachment.move.alreadyExists "
+                + "[attachment.txt, Space.Target\"', /xwiki/bin/view/Space/Target%22%27]",
             render.getElementsByClass("errormessage").get(0).text());
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MoveStatusPagesTest.java b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MoveStatusPagesTest.java
new file mode 100644
index 000000000000..9ce95cf51d94
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MoveStatusPagesTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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 org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.xwiki.job.Request;
+import org.xwiki.job.event.status.JobProgress;
+import org.xwiki.job.event.status.JobStatus;
+import org.xwiki.job.script.JobScriptService;
+import org.xwiki.script.service.ScriptService;
+import org.xwiki.template.TemplateManager;
+import org.xwiki.template.script.TemplateScriptService;
+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.xml.internal.html.filter.ControlCharactersFilter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test of the macros provided by {@code moveStatus.vm}.
+ *
+ * @version $Id$
+ * @since 14.4RC1
+ */
+@HTML50ComponentList
+@XWikiSyntax21ComponentList
+@ComponentList({
+    ControlCharactersFilter.class,
+    TemplateScriptService.class
+})
+class MoveStatusPagesTest extends PageTest
+{
+    private static final String MOVE_STATUS_TEMPLATE = "attachment/moveStatus.vm";
+
+    private TemplateManager templateManager;
+
+    @Mock
+    private JobScriptService jobScriptService;
+
+    @BeforeEach
+    void setUp() throws Exception
+    {
+        this.templateManager = this.oldcore.getMocker().getInstance(TemplateManager.class);
+        this.componentManager.registerComponent(ScriptService.class, "job", this.jobScriptService);
+    }
+
+    @Test
+    void renderScriptStatusUnknown() throws Exception
+    {
+        this.request.put("moveId", "42");
+        Document render = Jsoup.parse(this.templateManager.render(MOVE_STATUS_TEMPLATE));
+        assertEquals("attachment.move.status.notFound", render.select(".errormessage").text());
+        verify(this.jobScriptService).getJobStatus(List.of("refactoring", "moveAttachment", "42"));
+    }
+
+    @Test
+    void renderScriptActionGet() throws Exception
+    {
+        JobStatus jobStatus = mock(JobStatus.class);
+        Request request = mock(Request.class);
+        JobProgress jobProgress = mock(JobProgress.class);
+
+        when(request.getId()).thenReturn(List.of("rq", "id"));
+        when(jobProgress.getOffset()).thenReturn(10d);
+        when(jobStatus.getRequest()).thenReturn(request);
+        when(jobStatus.getState()).thenReturn(JobStatus.State.RUNNING);
+        when(jobStatus.getProgress()).thenReturn(jobProgress);
+        when(this.jobScriptService.getJobStatus(List.of("refactoring", "moveAttachment", "42")))
+            .thenReturn(jobStatus);
+        this.request.put("moveId", "42");
+        this.context.setAction("get");
+        assertEquals(
+            "{\"id\":[\"rq\",\"id\"],\"state\":\"RUNNING\",\"progress\":{\"offset\":10.0},"
+                + "\"log\":{\"offset\":0,\"items\":[]},\"message\":\"        \",\"questionTimeLeft\":0}",
+            this.templateManager.render(MOVE_STATUS_TEMPLATE).trim());
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MoveStep1PageTest.java b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MoveStep1PageTest.java
new file mode 100644
index 000000000000..a4111af54e6d
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/MoveStep1PageTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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 org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.xwiki.csrf.script.CSRFTokenScriptService;
+import org.xwiki.model.reference.AttachmentReference;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.script.service.ScriptService;
+import org.xwiki.security.script.SecurityScriptServiceComponentList;
+import org.xwiki.template.TemplateManager;
+import org.xwiki.test.page.PageTest;
+import org.xwiki.velocity.VelocityManager;
+
+import com.xpn.xwiki.doc.XWikiDocument;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test of {@code moveStep1.vm} template.
+ *
+ * @version $Id$
+ * @since 14.4RC1
+ */
+@SecurityScriptServiceComponentList
+class MoveStep1PageTest extends PageTest
+{
+    private static final String MOVE_STEP1_TEMPLATE = "attachment/moveStep1.vm";
+
+    private static final String ATTACHMENT_NAME = ">a<b>tt</b>achmen\"'t1.txt";
+
+    private static final DocumentReference DOC_REF = new DocumentReference("xwiki", "Space", "Page");
+
+    private TemplateManager templateManager;
+
+    @Mock
+    private CSRFTokenScriptService csrfScriptService;
+
+    private VelocityManager velocityManager;
+
+    @BeforeEach
+    void setUp() throws Exception
+    {
+        this.templateManager = this.oldcore.getMocker().getInstance(TemplateManager.class);
+        this.velocityManager = this.oldcore.getMocker().getInstance(VelocityManager.class);
+        this.componentManager.registerComponent(ScriptService.class, "csrf", this.csrfScriptService);
+        when(this.csrfScriptService.getToken()).thenReturn("csrf_token");
+    }
+
+    @Test
+    void escapedValues() throws Exception
+    {
+        XWikiDocument document = this.xwiki.getDocument(DOC_REF, this.context);
+        this.context.setDoc(document);
+
+        this.velocityManager.getVelocityContext().put("attachment", new AttachmentReference(ATTACHMENT_NAME,
+            document.getDocumentReference()));
+
+        Document render = Jsoup.parse(this.templateManager.render(MOVE_STEP1_TEMPLATE));
+        assertNotNull(render);
+        assertEquals("cancel", render.select(".buttons .secondary.button").text());
+        assertEquals("attachment.move.submit", render.select(".buttons input").attr("value"));
+        assertEquals("attachment.move.autoRedirect.label", render.select("dt.autoRedirect label").text());
+        assertEquals("attachment.move.autoRedirect.hint", render.select("dd.autoRedirect .xHint").text());
+        assertEquals("attachment.move.newName.label", render.select("dt.targetAttachmentName label").text());
+        assertEquals("attachment.move.newName.hint", render.select("dt.targetAttachmentName .xHint").text());
+        assertEquals(ATTACHMENT_NAME, render.select("[name='sourceAttachmentName']").attr("value"));
+        assertEquals(ATTACHMENT_NAME, render.select("#targetAttachmentNameTitle").attr("value"));
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/RefactoringMacrosPageTest.java b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/RefactoringMacrosPageTest.java
new file mode 100644
index 000000000000..42052ed44515
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/RefactoringMacrosPageTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.stream.Stream;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.xwiki.attachment.script.AttachmentScriptService;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.rendering.syntax.Syntax;
+import org.xwiki.script.service.ScriptService;
+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.xml.internal.html.filter.ControlCharactersFilter;
+
+import com.xpn.xwiki.doc.XWikiDocument;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test of the macros provided by {@code refactoring_macros.vm}.
+ *
+ * @version $Id$
+ * @since 14.4RC1
+ */
+@ComponentList({ ControlCharactersFilter.class })
+@HTML50ComponentList
+@XWikiSyntax21ComponentList
+class RefactoringMacrosPageTest extends PageTest
+{
+    public static final DocumentReference DOCUMENT_REFERENCE = new DocumentReference("xwiki", "Space", "Page");
+
+    @ParameterizedTest
+    @CsvSource({
+        "false,false,true",
+        "false,true,false",
+        "true,false,false",
+        "true,true, false"
+    })
+    void displayAttachmentLinksCheckboxIsHidden(boolean isAdvancedUser, boolean isSuperAdmin, boolean expectHidden)
+        throws Exception
+    {
+        XWikiDocument document = this.xwiki.getDocument(DOCUMENT_REFERENCE, this.context);
+        document.setContent(String.format("{{velocity}}\n"
+                + "#template('attachment/refactoring_macros.vm')\n"
+                + "#set($isAdvancedUser = %s)\n"
+                + "#set($isSuperAdmin = %s)\n"
+                + "{{html}}#displayAttachmentLinksCheckbox(){{/html}}\n"
+                + "{{/velocity}}",
+            isAdvancedUser, isSuperAdmin));
+        document.setSyntax(Syntax.XWIKI_2_1);
+        this.xwiki.saveDocument(document, this.context);
+        Document content = Jsoup.parse(document.getRenderedContent(this.context));
+        assertEquals(expectHidden, content.select("dt").hasClass("hidden"));
+        assertEquals(expectHidden, content.select("dd").hasClass("hidden"));
+    }
+
+    @ParameterizedTest
+    @MethodSource("displayAttachmentLinksCheckboxSource")
+    void displayAttachmentLinksCheckbox(String updateReferences, String expectedChecked)
+        throws Exception
+    {
+        AttachmentScriptService attachmentScriptService = mock(AttachmentScriptService.class);
+        this.componentManager.registerComponent(ScriptService.class, "attachment", attachmentScriptService);
+        when(attachmentScriptService.backlinksCount(any())).thenReturn(10L);
+
+        XWikiDocument document = this.xwiki.getDocument(DOCUMENT_REFERENCE, this.context);
+        document.setContent("{{velocity}}\n"
+            + "#template('attachment/refactoring_macros.vm')\n"
+            + "{{html}}#displayAttachmentLinksCheckbox(){{/html}}\n"
+            + "{{/velocity}}");
+        document.setSyntax(Syntax.XWIKI_2_1);
+        this.xwiki.saveDocument(document, this.context);
+
+        this.request.put("updateReferences", updateReferences);
+
+        Document parse = Jsoup.parse(document.getRenderedContent(this.context));
+        // The checkbox is checked by default, unless updateReferences is defined to value different than 'true'.
+        assertEquals(expectedChecked, parse.select("[name='updateReferences']").attr("checked"));
+        assertEquals("attachment.move.links.hint [10]", parse.select(".xHint").text());
+    }
+
+    /**
+     * @return the arguments for {@link #displayAttachmentLinksCheckbox(String, String)}
+     */
+    private static Stream<Arguments> displayAttachmentLinksCheckboxSource()
+    {
+        return Stream.of(
+            Arguments.of(null, "checked"),
+            Arguments.of("", ""),
+            Arguments.of("false", ""),
+            Arguments.of("true", "checked")
+        );
+    }
+}

Vulnerable code:         #error($services.localization.render('attachment.move.emptyName'))
          #error($services.localization.render('attachment.move.targetNotWritable',
            [${escapetool.xml($targetLocation)}]))
            #displayException($services.localization.render('attachment.job.creation.error'), $moveJobException)
  <h2>$services.localization.render('attachment.move.status.label')</h2>
  <p class="text-muted small">$services.localization.render('attachment.move.status.hint',
  [$xwiki.getUserName($moveJobRequest.userReference), $xwiki.formatDate($moveJobStatus.startDate)])</p>
    #outputMoveJobStatusJSON($moveJobStatus)
  $services.localization.render('attachment.move.status.notFound')
#set($titleToDisplay = $services.localization.render('attachment.move.title', 
  [$attachment.name, $escapetool.xml($doc.plainTitle), $doc.getURL()]))
        <dt>
            $services.localization.render('attachment.move.autoRedirect.label')
        <dd>
          <span class="xHint">$services.localization.render('attachment.move.autoRedirect.hint')</span>
        <dt>
          <label for="targetAttachmentName">$services.localization.render('attachment.move.newName.label')</label>
          <span class="xHint">$!services.localization.render('attachment.move.newName.hint')</span>
          <input type="text" id="targetAttachmentNameTitle" name="targetAttachmentName" value="$escapetool.xml($attachment.name)"
                 placeholder="$!services.localization.render('attachment.move.newName.placeholder')" /> 
          <label for="$!{options.id}Title">$services.localization.render('attachment.move.newLocation.label')</label>
          <span class="xHint">$!services.localization.render('attachment.move.newLocation.hint')</span>
                  <h5 class="modal-title">Modal title</h5>
                  <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                  <button type="button" class="btn btn-primary">Select</button>
      <input type="submit" value="$services.localization.render('attachment.move.submit')" class="button" />
      <a class="secondary button" href="$doc.getURL()">$services.localization.render('cancel')</a>
  <label>$services.localization.render('attachment.move.source.label')</label>
  <span class="xHint">$services.localization.render('attachment.move.source.hint')</span>
    $services.localization.render('attachment.move.links.label')
    $services.localization.render('attachment.move.links.hint', [$backLinksCount])
        assertEquals("error: attachment.move.targetNotWritable [Space.Target]",
        DocumentReference targetDocumentReference = new DocumentReference("xwiki", "Space", "Target");
        this.request.put("targetLocation", "Space.Target");
        assertEquals(
            "error: attachment.move.alreadyExists [attachment.txt, Space.Target, /xwiki/bin/view/Space/Target]",
Fixed code: attachment.move.targetLocation.modal.title=Select New Attachment Location
attachment.move.targetLocation.modal.selectButton=Select
attachment.move.targetLocation.modal.closeButton=Close
        #error($escapetool.xml($services.localization.render('attachment.move.emptyName')))
          #error($escapetool.xml($services.localization.render('attachment.move.targetNotWritable')))
            #displayException($escapetool.xml($services.localization.render('attachment.job.creation.error')), $moveJobException)
  <h2>$escapetool.xml($services.localization.render('attachment.move.status.label'))</h2>
  <p class="text-muted small">
    $escapetool.xml($services.localization.render('attachment.move.status.hint', 
      [$xwiki.getUserName($moveJobRequest.userReference), $xwiki.formatDate($moveJobStatus.startDate)]))
  </p>
    #outputmoveJobStatusJSON($moveJobStatus)
  $escapetool.xml($services.localization.render('attachment.move.status.notFound'))
#set($titleToDisplay = $services.localization.render('attachment.move.title', [
  $escapetool.xml($attachment.name), 
  $escapetool.xml($doc.plainTitle), 
  $escapetool.xml($doc.getURL())
]))
        <dt class="autoRedirect">
            $escapetool.xml($services.localization.render('attachment.move.autoRedirect.label'))
        <dd class="autoRedirect">
          <span class="xHint">
            $escapetool.xml($services.localization.render('attachment.move.autoRedirect.hint'))
          </span>
        <dt class="targetAttachmentName">
          <label for="targetAttachmentNameTitle">
            $escapetool.xml($services.localization.render('attachment.move.newName.label'))
          </label>
          <span class="xHint">
            $escapetool.xml($services.localization.render('attachment.move.newName.hint'))
          </span>
          <input type="text" 
                 id="targetAttachmentNameTitle" 
                 name="targetAttachmentName" 
                 value="$escapetool.xml($attachment.name)"
                 placeholder="$escapetool.xml($services.localization.render('attachment.move.newName.placeholder'))" /> 
          <label for="targetLocation">
              $escapetool.xml($services.localization.render('attachment.move.newLocation.label'))
          </label>
          <span class="xHint">
            $escapetool.xml($services.localization.render('attachment.move.newLocation.hint'))
          </span>
                  <h5 class="modal-title">
                    $escapetool.xml($services.localization.render('attachment.move.targetLocation.modal.title'))
                  </h5>
                  <button type="button" class="btn btn-default" data-dismiss="modal">
                    $escapetool.xml($services.localization.render('attachment.move.targetLocation.modal.closeButton'))
                  </button>
                  <button type="button" class="btn btn-primary">
                    $escapetool.xml($services.localization.render('attachment.move.targetLocation.modal.selectButton'))
                  </button>
      <input type="submit" 
             value="$escapetool.xml($services.localization.render('attachment.move.submit'))" 
             class="button" />
      <a class="secondary button" href="$doc.getURL()">
        $escapetool.xml($services.localization.render('cancel'))
      </a>
  <label>
    $escapetool.xml($services.localization.render('attachment.move.source.label'))
  </label>
  <span class="xHint">
    $escapetool.xml($services.localization.render('attachment.move.source.hint'))
  </span>
    $escapetool.xml($services.localization.render('attachment.move.links.label'))
  $escapetool.xml($services.localization.render('attachment.move.links.hint', [$backLinksCount]))
        assertEquals("error: attachment.move.targetNotWritable",
    @Test
    void submitTargetAttachmentNameEmpty() throws Exception
    {
        this.context.setDoc(this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "Source"), this.context));
        this.request.put("step", "2");
        this.request.put("form_token", "a6DSv7pKWcPargoTvyx2Ww");
        Document render = Jsoup.parse(this.templateManager.render(MOVE_TEMPLATE));
        assertEquals("error: attachment.move.emptyName", render.select(".errormessage").text());
    }

        DocumentReference targetDocumentReference = new DocumentReference("xwiki", "Space", "Target\"'");
        this.request.put("targetLocation", "Space.Target\"'");
        assertEquals("error: attachment.move.alreadyExists "
                + "[attachment.txt, Space.Target\"', /xwiki/bin/view/Space/Target%22%27]",
/*
 * 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 org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.xwiki.job.Request;
import org.xwiki.job.event.status.JobProgress;
import org.xwiki.job.event.status.JobStatus;
import org.xwiki.job.script.JobScriptService;
import org.xwiki.script.service.ScriptService;
import org.xwiki.template.TemplateManager;
import org.xwiki.template.script.TemplateScriptService;
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.xml.internal.html.filter.ControlCharactersFilter;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Test of the macros provided by {@code moveStatus.vm}.
 *
 * @version $Id$
 * @since 14.4RC1
 */
@HTML50ComponentList
@XWikiSyntax21ComponentList
@ComponentList({
    ControlCharactersFilter.class,
    TemplateScriptService.class
})
class MoveStatusPagesTest extends PageTest
{
    private static final String MOVE_STATUS_TEMPLATE = "attachment/moveStatus.vm";

    private TemplateManager templateManager;

    @Mock
    private JobScriptService jobScriptService;

    @BeforeEach
    void setUp() throws Exception
    {
        this.templateManager = this.oldcore.getMocker().getInstance(TemplateManager.class);
        this.componentManager.registerComponent(ScriptService.class, "job", this.jobScriptService);
    }

    @Test
    void renderScriptStatusUnknown() throws Exception
    {
        this.request.put("moveId", "42");
        Document render = Jsoup.parse(this.templateManager.render(MOVE_STATUS_TEMPLATE));
        assertEquals("attachment.move.status.notFound", render.select(".errormessage").text());
        verify(this.jobScriptService).getJobStatus(List.of("refactoring", "moveAttachment", "42"));
    }

    @Test
    void renderScriptActionGet() throws Exception
    {
        JobStatus jobStatus = mock(JobStatus.class);
        Request request = mock(Request.class);
        JobProgress jobProgress = mock(JobProgress.class);

        when(request.getId()).thenReturn(List.of("rq", "id"));
        when(jobProgress.getOffset()).thenReturn(10d);
        when(jobStatus.getRequest()).thenReturn(request);
        when(jobStatus.getState()).thenReturn(JobStatus.State.RUNNING);
        when(jobStatus.getProgress()).thenReturn(jobProgress);
        when(this.jobScriptService.getJobStatus(List.of("refactoring", "moveAttachment", "42")))
            .thenReturn(jobStatus);
        this.request.put("moveId", "42");
        this.context.setAction("get");
        assertEquals(
            "{\"id\":[\"rq\",\"id\"],\"state\":\"RUNNING\",\"progress\":{\"offset\":10.0},"
                + "\"log\":{\"offset\":0,\"items\":[]},\"message\":\"        \",\"questionTimeLeft\":0}",
            this.templateManager.render(MOVE_STATUS_TEMPLATE).trim());
    }
}
/*
 * 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 org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.xwiki.csrf.script.CSRFTokenScriptService;
import org.xwiki.model.reference.AttachmentReference;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.script.service.ScriptService;
import org.xwiki.security.script.SecurityScriptServiceComponentList;
import org.xwiki.template.TemplateManager;
import org.xwiki.test.page.PageTest;
import org.xwiki.velocity.VelocityManager;

import com.xpn.xwiki.doc.XWikiDocument;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;

/**
 * Test of {@code moveStep1.vm} template.
 *
 * @version $Id$
 * @since 14.4RC1
 */
@SecurityScriptServiceComponentList
class MoveStep1PageTest extends PageTest
{
    private static final String MOVE_STEP1_TEMPLATE = "attachment/moveStep1.vm";

    private static final String ATTACHMENT_NAME = ">a<b>tt</b>achmen\"'t1.txt";

    private static final DocumentReference DOC_REF = new DocumentReference("xwiki", "Space", "Page");

    private TemplateManager templateManager;

    @Mock
    private CSRFTokenScriptService csrfScriptService;

    private VelocityManager velocityManager;

    @BeforeEach
    void setUp() throws Exception
    {
        this.templateManager = this.oldcore.getMocker().getInstance(TemplateManager.class);
        this.velocityManager = this.oldcore.getMocker().getInstance(VelocityManager.class);
        this.componentManager.registerComponent(ScriptService.class, "csrf", this.csrfScriptService);
        when(this.csrfScriptService.getToken()).thenReturn("csrf_token");
    }

    @Test
    void escapedValues() throws Exception
    {
        XWikiDocument document = this.xwiki.getDocument(DOC_REF, this.context);
        this.context.setDoc(document);

        this.velocityManager.getVelocityContext().put("attachment", new AttachmentReference(ATTACHMENT_NAME,
            document.getDocumentReference()));

        Document render = Jsoup.parse(this.templateManager.render(MOVE_STEP1_TEMPLATE));
        assertNotNull(render);
        assertEquals("cancel", render.select(".buttons .secondary.button").text());
        assertEquals("attachment.move.submit", render.select(".buttons input").attr("value"));
        assertEquals("attachment.move.autoRedirect.label", render.select("dt.autoRedirect label").text());
        assertEquals("attachment.move.autoRedirect.hint", render.select("dd.autoRedirect .xHint").text());
        assertEquals("attachment.move.newName.label", render.select("dt.targetAttachmentName label").text());
        assertEquals("attachment.move.newName.hint", render.select("dt.targetAttachmentName .xHint").text());
        assertEquals(ATTACHMENT_NAME, render.select("[name='sourceAttachmentName']").attr("value"));
        assertEquals(ATTACHMENT_NAME, render.select("#targetAttachmentNameTitle").attr("value"));
    }
}
/*
 * 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.stream.Stream;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.xwiki.attachment.script.AttachmentScriptService;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.rendering.syntax.Syntax;
import org.xwiki.script.service.ScriptService;
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.xml.internal.html.filter.ControlCharactersFilter;

import com.xpn.xwiki.doc.XWikiDocument;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Test of the macros provided by {@code refactoring_macros.vm}.
 *
 * @version $Id$
 * @since 14.4RC1
 */
@ComponentList({ ControlCharactersFilter.class })
@HTML50ComponentList
@XWikiSyntax21ComponentList
class RefactoringMacrosPageTest extends PageTest
{
    public static final DocumentReference DOCUMENT_REFERENCE = new DocumentReference("xwiki", "Space", "Page");

    @ParameterizedTest
    @CsvSource({
        "false,false,true",
        "false,true,false",
        "true,false,false",
        "true,true, false"
    })
    void displayAttachmentLinksCheckboxIsHidden(boolean isAdvancedUser, boolean isSuperAdmin, boolean expectHidden)
        throws Exception
    {
        XWikiDocument document = this.xwiki.getDocument(DOCUMENT_REFERENCE, this.context);
        document.setContent(String.format("{{velocity}}\n"
                + "#template('attachment/refactoring_macros.vm')\n"
                + "#set($isAdvancedUser = %s)\n"
                + "#set($isSuperAdmin = %s)\n"
                + "{{html}}#displayAttachmentLinksCheckbox(){{/html}}\n"
                + "{{/velocity}}",
            isAdvancedUser, isSuperAdmin));
        document.setSyntax(Syntax.XWIKI_2_1);
        this.xwiki.saveDocument(document, this.context);
        Document content = Jsoup.parse(document.getRenderedContent(this.context));
        assertEquals(expectHidden, content.select("dt").hasClass("hidden"));
        assertEquals(expectHidden, content.select("dd").hasClass("hidden"));
    }

    @ParameterizedTest
    @MethodSource("displayAttachmentLinksCheckboxSource")
    void displayAttachmentLinksCheckbox(String updateReferences, String expectedChecked)
        throws Exception
    {
        AttachmentScriptService attachmentScriptService = mock(AttachmentScriptService.class);
        this.componentManager.registerComponent(ScriptService.class, "attachment", attachmentScriptService);
        when(attachmentScriptService.backlinksCount(any())).thenReturn(10L);

        XWikiDocument document = this.xwiki.getDocument(DOCUMENT_REFERENCE, this.context);
        document.setContent("{{velocity}}\n"
            + "#template('attachment/refactoring_macros.vm')\n"
            + "{{html}}#displayAttachmentLinksCheckbox(){{/html}}\n"
            + "{{/velocity}}");
        document.setSyntax(Syntax.XWIKI_2_1);
        this.xwiki.saveDocument(document, this.context);

        this.request.put("updateReferences", updateReferences);

        Document parse = Jsoup.parse(document.getRenderedContent(this.context));
        // The checkbox is checked by default, unless updateReferences is defined to value different than 'true'.
        assertEquals(expectedChecked, parse.select("[name='updateReferences']").attr("checked"));
        assertEquals("attachment.move.links.hint [10]", parse.select(".xHint").text());
    }

    /**
     * @return the arguments for {@link #displayAttachmentLinksCheckbox(String, String)}
     */
    private static Stream<Arguments> displayAttachmentLinksCheckboxSource()
    {
        return Stream.of(
            Arguments.of(null, "checked"),
            Arguments.of("", ""),
            Arguments.of("false", ""),
            Arguments.of("true", "checked")
        );
    }
}