CVE-2023-29522: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (CWE-74)
value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. Any user with view rights can execute arbitrary script macros including Groovy and Python macros that allow remote code execution including unrestricted read and write access to all wiki contents. The attack works by opening a non-existing page with a name crafted to contain a dangerous payload. This issue has been patched in XWiki 14.4.8, 14.10.3 and 15.0RC1. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
CVSS v3 base score: 8.8
Published: 2023-04-19T00:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: XML
Files changed: xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/locationPicker_macros.vm, xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/pom.xml, xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/src/main/resources/XWiki/ClassSheet.xml, xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/src/test/java/org/xwiki/xclass/ClassSheetPageTest.java
Commit date: 2023-01-03 17:37:10 +0100
Commit message: XWIKI-20456: Improved escaping of XWiki.ClassSheet

Diff: diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/locationPicker_macros.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/locationPicker_macros.vm
index 3e06b7c3a512a..b08dbd799b987 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/locationPicker_macros.vm
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/locationPicker_macros.vm
@@ -38,16 +38,18 @@
       #set ($value = $titleField.value)
     #end
     #set ($escapedValue = $escapetool.xml($value))
     #if ($titleField.label)
       <dt>
-        <label for="$!{options.id}Title">$services.localization.render($titleField.label)</label>
-        <span class="xHint">$!services.localization.render($titleField.hint)</span>
+        <label for="$escapetool.xml($!{options.id})Title">##
+          $escapetool.xml($services.localization.render($titleField.label))##
+        </label>
+        <span class="xHint">$!escapetool.xml($services.localization.render($titleField.hint))</span>
       </dt>
       <dd>
-        <input type="text" id="$!{options.id}Title" name="$titleField.name" value="$!escapedValue"
-          class="location-title-field" placeholder="$!services.localization.render($titleField.placeholder)" />
+        <input type="text" id="$escapetool.xml($!{options.id})Title" name="$escapetool.xml($titleField.name)" value="$!escapedValue"
+          class="location-title-field" placeholder="$escapetool.xml($!services.localization.render($titleField.placeholder))" />
       </dd>
     #elseif ($titleField)
       <dt class="hidden"></dt>
       <dd class="hidden">
         <input type="hidden" name="$titleField.name" value="$!escapedValue"
@@ -58,12 +60,12 @@
     ## ---------------------------------------------------------------------------------------------------------
     ## Location preview
     ## ---------------------------------------------------------------------------------------------------------
     ##
     <dt>
-      <label>$services.localization.render($options.preview.label)</label>
-      <span class="xHint">$services.localization.render($options.preview.hint)</span>
+      <label>$escapetool.xml($services.localization.render($options.preview.label))</label>
+      <span class="xHint">$escapetool.xml($services.localization.render($options.preview.hint))</span>
     </dt>
     <dd>
       #if ($isDocumentTreeAvailable)
         #documentPickerModal($options)
         #locationPickerActions()
@@ -111,12 +113,14 @@
         #set ($value = $wikiField.value)
       #end
       #set ($escapedValue = $escapetool.xml($value))
       #if ($wikiField.label && $displayWikiFields)
         <dt>
-          <label for="$!{options.id}Wiki">$services.localization.render($wikiField.label)</label>
-          <span class="xHint">$!services.localization.render($wikiField.hint)</span>
+          <label for="$escapetool.xml($!{options.id})Wiki">##
+            $escapetool.xml($services.localization.render($wikiField.label))##
+          </label>
+          <span class="xHint">$!escapetool.xml($services.localization.render($wikiField.hint))</span>
         </dt>
         <dd>
           <select id="$!{options.id}Wiki" name="$wikiField.name" class="location-wiki-field">
             #set ($wikis = $xwiki.wikiNames)
             #foreach ($wiki in $wikis)
@@ -154,17 +158,19 @@
         #set ($defaultParentReferenceString = $services.model.serialize($defaultParentReference, 'local'))
         #set ($value = $defaultParentReferenceString)
       #end
       #set ($escapedValue = $escapetool.xml($value))
       <dt>
-        <label for="$!{options.id}ParentReference">$services.localization.render($parentField.label)</label>
-        <span class="xHint">$!services.localization.render($parentField.hint)</span>
+        <label for="$escapetool.xml($!{options.id})ParentReference">##
+          $escapetool.xml($services.localization.render($parentField.label))##
+        </label>
+        <span class="xHint">$!escapetool.xml($services.localization.render($parentField.hint))</span>
       </dt>
       <dd>
-        <input type="text" id="$!{options.id}ParentReference" class="location-parent-field suggestSpaces"
-          name="$parentField.name" value="$!escapedValue"
-          placeholder="$!services.localization.render($parentField.placeholder)" />
+        <input type="text" id="$escapetool.xml($!{options.id})ParentReference" class="location-parent-field suggestSpaces"
+          name="$escapetool.xml($parentField.name)" value="$!escapedValue"
+          placeholder="$!escapetool.xml($services.localization.render($parentField.placeholder))" />
       </dd>
       ##
       ## ---------------------------------------------------------------------------------------------------------
       ## Name field
       ## ---------------------------------------------------------------------------------------------------------
@@ -175,17 +181,19 @@
         #set ($value = $nameField.value)
       #end
       #set ($escapedValue = $escapetool.xml($value))
       #if ($nameField.label)
         <dt>
-          <label for="$!{options.id}Name">$services.localization.render($nameField.label)</label>
-          <span class="xHint">$!services.localization.render($nameField.hint)</span>
+          <label for="$escapetool.xml($!{options.id})Name">##
+            $escapetool.xml($services.localization.render($nameField.label))##
+          </label>
+          <span class="xHint">$escapetool.xml($services.localization.render($nameField.hint))</span>
         </dt>
         <dd>
-          <input type="text" id="$!{options.id}Name" name="$nameField.name" class="location-name-field"
+          <input type="text" id="$escapetool.xml($!{options.id})Name" name="$escapetool.xml($nameField.name)" class="location-name-field"
             value="$!escapedValue"
-            placeholder="$!services.localization.render($nameField.placeholder)" />
+            placeholder="$escapetool.xml($!services.localization.render($nameField.placeholder))" />
         </dd>
       #elseif ($nameField)
         <dt class="hidden"></dt>
         <dd class="hidden">
           <input type="hidden" name="$nameField.name" class="location-name-field"
diff --git a/xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/pom.xml b/xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/pom.xml
index 86a7a2bb8e5f7..a91e12f0d4514 100644
--- a/xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/pom.xml
@@ -83,7 +83,47 @@
       <groupId>org.xwiki.platform</groupId>
       <artifactId>xwiki-platform-livedata-livetable</artifactId>
       <version>${project.version}</version>
       <scope>runtime</scope>
     </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-rendering-xwiki</artifactId>
+      <version>${project.version}</version>
+      <scope>runtime</scope>
+    </dependency>
+    <!-- Test dependencies. -->
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-test-page</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-web-templates</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-livedata-macro</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+      <type>test-jar</type>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-rendering-xwiki</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+      <type>test-jar</type>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-rendering-configuration-default</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 </project>
diff --git a/xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/src/main/resources/XWiki/ClassSheet.xml b/xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/src/main/resources/XWiki/ClassSheet.xml
index 66048d612182d..5e7ef3f27512e 100644
--- a/xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/src/main/resources/XWiki/ClassSheet.xml
+++ b/xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/src/main/resources/XWiki/ClassSheet.xml
@@ -127,47 +127,61 @@
     #set($defaultParent = ${doc.fullName})
   #end
 
   #set ($classEditorURL = $doc.getURL('edit', 'editor=class'))
   #if($doc.getxWikiClass().properties.size() == 0)
-    {{warning}}$services.localization.render('platform.xclass.defaultClassSheet.properties.empty', [
-      "{{html}}&lt;a href='$classEditorURL'&gt;",
-      '&lt;/a&gt;{{/html}}'
-    ]){{/warning}}
+    #set ($openLink = "&lt;a href='$escapetool.xml($classEditorURL)'&gt;")
+    #set ($closeLink = '&lt;/a&gt;')
+    {{warning}}
+      {{html}}
+      ## First escape the content of the translation, then replace the placeholders with content that would otherwise be
+      ## escaped during the first escaping.
+      #set ($warningMessage = $services.localization.render('platform.xclass.defaultClassSheet.properties.empty', 
+        ['__OPEN_LINK__', '__CLOSE_LINK__']))
+      $escapetool.xml($warningMessage).replace('__OPEN_LINK__', $openLink).replace('__CLOSE_LINK__', $closeLink)
+      {{/html}}
+    {{/warning}}
   #else
     (% id="HClassProperties" %)
     = {{translation key="platform.xclass.defaultClassSheet.properties.heading"/}} =
     #foreach($property in $doc.getxWikiClass().properties)
       * $services.rendering.escape("$property.prettyName ($property.name: $xwiki.metaclass.get($property.classType).prettyName)", $xwiki.currentContentSyntaxId)
     #end
-    * //$services.localization.render('platform.xclass.defaultClassSheet.properties.edit', [
-      "{{html}}&lt;a href='$classEditorURL'&gt;",
-      '&lt;/a&gt;{{/html}}'
-    ])//
+    #set ($openLink = "&lt;a href='$escapetool.xml($classEditorURL)'&gt;")
+    #set ($closeLink = '&lt;/a&gt;')
+    #set ($warningMessage = $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.properties.edit', ['__OPEN_LINK__', '__CLOSE_LINK__'])))
+    ## First escape the content of the translation, then replace the placeholders with content that would otherwise be
+    ## escaped during the first escaping.
+    * //{{html}}$warningMessage.replace('__OPEN_LINK__', $openLink).replace('__CLOSE_LINK__', $closeLink){{/html}}//
 
   #end
   #if ($hasClassSheets &amp;&amp; $hasClassTemplate)
     (% id="HCreatePage" %)
     = {{translation key="platform.xclass.defaultClassSheet.createPage.heading"/}} =
     #if("$!targetDocRef" != '' &amp;&amp; $xwiki.exists($targetDocRef))
-
-      {{warning}}$services.localization.render('platform.xclass.defaultClassSheet.createPage.pageAlreadyExists', [
-        '[[',
-        "&gt;&gt;$services.model.serialize($targetDocRef)]]"
-      ]){{/warning}}
+      {{warning}}
+        {{html}}
+          #set ($targetDocLink = $xwiki.getURL($targetDocRef))
+          #set ($openLink = "&lt;a href='$escapetool.xml($targetDocLink)'&gt;")
+          #set ($message = $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.createPage.pageAlreadyExists', ['__OPEN_LINK__', '__CLOSE_LINK__'])))
+          ## First escape the content of the translation, then replace the placeholders with content that would
+          ## otherwise be escaped during the first escaping.
+          $message.replace('__OPEN_LINK__', $openLink).replace('__CLOSE_LINK__', '&lt;/a&gt;')
+        {{/html}}
+      {{/warning}}
     #elseif("$!targetDocRef" != '')
 
       {{warning}}{{translation key="platform.xclass.defaultClassSheet.createPage.denied"/}}{{/warning}}
     #end
 
     {{html}}
     &lt;form action="$doc.getURL()" id="newdoc" method="post" class="xform half"&gt;
       &lt;fieldset&gt;
       &lt;div class="hidden"&gt;
         &lt;input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" /&gt;
-        &lt;input type="hidden" name="parent" value="${defaultParent}"/&gt;
-        &lt;input type="hidden" name="template" value="${classTemplateDoc}"/&gt;
+        &lt;input type="hidden" name="parent" value="$escapetool.xml(${defaultParent})"/&gt;
+        &lt;input type="hidden" name="template" value="$escapetool.xml(${classTemplateDoc})"/&gt;
         &lt;input type="hidden" name="sheet" value="1"/&gt;
       &lt;/div&gt;
       #locationPicker({
         'id': 'target',
         'title': {
@@ -217,12 +231,12 @@
   })
   {{liveData
     id="classEntries"
     properties="doc.title,doc.location,doc.date,doc.author,doc.objectCount,_actions"
     source="liveTable"
-    className="${doc.fullName}"
-    sourceParameters="${escapetool.url($options)}"
+    className="$services.rendering.escape(${doc.fullName}, 'xwiki/2.1')"
+    sourceParameters="$services.rendering.escape($escapetool.url($options), 'xwiki/2.1')"
   }}
   {
     "meta": {
       "propertyDescriptors": [
         {
@@ -245,19 +259,25 @@
   #if (!$hasClassSheets || !$hasClassTemplate)
 
     {{translation key="platform.xclass.defaultClassSheet.sheets.missing"/}}
   #end
 
-  {{info}}$services.localization.render('platform.xclass.defaultClassSheet.sheets.description', ['//', '//']){{/info}}
+  {{info}}
+    #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.sheets.description', ['__START_EM__', '__END_EM__']))
+    #set ($message = $escapetool.xml($message))
+    ## First escape the content of the translation, then replace the placeholders with content that would
+    ## otherwise be escaped during the first escaping.
+    {{html}}$message.replace('__START_EM__', '&lt;em&gt;').replace('__END_EM__', '&lt;/em&gt;'){{/html}}
+  {{/info}}
 
   #if(!$hasClassSheets)
     {{html}}
       &lt;form action="$xwiki.getURL($defaultClassSheetReference, 'save', 'editor=wiki')" method="post"&gt;
         &lt;div&gt;
           &lt;input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" /&gt;
-          &lt;input type="hidden" name="parent" value="${doc.fullName}"/&gt;
-          &lt;input type="hidden" name="xredirect" value="${doc.URL}"/&gt;
+          &lt;input type="hidden" name="parent" value="$escapetool.xml(${doc.fullName})"/&gt;
+          &lt;input type="hidden" name="xredirect" value="$escapetool.xml(${doc.URL})"/&gt;
           #set ($sheetContent = $xwiki.getDocument('XWiki.ObjectSheet').getContent().replace('XWiki.MyClass',
             $doc.fullName))
           ## We have to encode the new line characters in order to preserve them, otherwise they are replace with a
           ## space when the HTML is cleaned.
           ## FIXME: Use a dedicated escape tool method when XCOMMONS-405 is implemented.
@@ -278,11 +298,13 @@
       #set($bindURL = $doc.getURL('view', "bindSheet=${escapetool.url($defaultClassSheetStringReference)}&amp;xredirect=${escapetool.url($xredirect)}&amp;form_token=$!{services.csrf.getToken()}"))
       {{warning}}
         {{translation key="platform.xclass.defaultClassSheet.sheets.notBound"/}} ##
         #if ($hasEdit)
           {{html}}
-          &lt;a href="$bindURL"&gt;$services.localization.render('platform.xclass.defaultClassSheet.sheets.bind') »&lt;/a&gt;.
+          &lt;a href="$escapetool.xml($bindURL)"&gt;##
+            $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.sheets.bind')) »##
+          &lt;/a&gt;.
           {{/html}}
         #end
       {{/warning}}
 
     #end
@@ -290,11 +312,16 @@
       #set($classSheetDoc = $defaultClassSheetDoc)
       #if(!$classSheetReferences.isEmpty())
         #set($classSheetDoc = $xwiki.getDocument($classSheetReferences.get(0)))
       #end
       #set ($sheetPath = "#hierarchy($classSheetDoc.documentReference, {'plain': true, 'local': true, 'limit': 4})")
-      [[$services.localization.render('platform.xclass.defaultClassSheet.sheets.view', [$sheetPath.trim()]) »&gt;&gt;${classSheetDoc.fullName}]]
+      #set ($classSheetLink = "$services.localization.render('platform.xclass.defaultClassSheet.sheets.view', [$sheetPath.trim()]) »")
+      #set ($classSheetLink = $services.rendering.escape($classSheetLink, 'xwiki/2.1'))
+      #set ($classSheetLink = $services.rendering.escape($classSheetLink, 'xwiki/2.1'))
+      #set ($classSheetText = ${classSheetDoc.fullName})
+      #set ($classSheetText = $services.rendering.escape($classSheetText, 'xwiki/2.1'))
+      [[$classSheetLink&gt;&gt;$classSheetText]]
     #else
       {{translation key="platform.xclass.defaultClassSheet.sheets.list"/}}
 
       #foreach($classSheetReference in $classSheetReferences)
         * [[$services.model.serialize($classSheetReference, "default")]]
@@ -303,48 +330,72 @@
   #end
 
   (% id="HClassTemplate" %)
   = {{translation key="platform.xclass.defaultClassSheet.template.heading"/}} =
 
-    {{info}}$services.localization.render('platform.xclass.defaultClassSheet.template.description',
-      ['//', '//']){{/info}}
+    {{info}}
+      #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.template.description', ['__START_EM__', '__END_EM__']))
+      #set ($message = $escapetool.xml($message))
+      ## First escape the content of the translation, then replace the placeholders with content that would
+      ## otherwise be escaped during the first escaping.
+      {{html}}$message.replace('__START_EM__', '&lt;em&gt;').replace('__END_EM__', '&lt;/em&gt;'){{/html}}
+    {{/info}}
 
   #if (!$hasClassTemplate)
     {{html}}
-      &lt;form action="$classTemplateDoc.getURL('save', 'editor=wiki')" method="post"&gt;
+      &lt;form action="$escapetool.xml($classTemplateDoc.getURL('save', 'editor=wiki'))" method="post"&gt;
         &lt;div&gt;
           &lt;input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" /&gt;
-          &lt;input type="hidden" name="parent" value="${doc.fullName}"/&gt;
-          &lt;input type="hidden" name="xredirect" value="${doc.URL}"/&gt;
-          &lt;input type="hidden" name="title" value="$className Template"/&gt;
+          &lt;input type="hidden" name="parent" value="$escapetool.xml(${doc.fullName})"/&gt;
+          &lt;input type="hidden" name="xredirect" value="$escapetool.xml(${doc.URL})"/&gt;
+          &lt;input type="hidden" name="title" value="$escapetool.xml($className) Template"/&gt;
           &lt;span class="buttonwrapper"&gt;&lt;input type="submit" class="button" value="$escapetool.xml(
             $services.localization.render('platform.xclass.defaultClassSheet.template.create'))"/&gt;&lt;/span&gt;
         &lt;/div&gt;
       &lt;/form&gt;
     {{/html}}
   #else
     #if(!$classTemplateDoc.getObject(${doc.fullName}))
       #set($xredirect = $xwiki.relativeRequestURL)
-      #set($createUrl = $classTemplateDoc.getURL('objectadd', "classname=${escapetool.url($doc.fullName)}&amp;amp;xredirect=${escapetool.url($xredirect)}&amp;amp;form_token=$!{services.csrf.getToken()}"))
+      #set($createUrl = $classTemplateDoc.getURL('objectadd', "classname=${escapetool.url($doc.fullName)}&amp;xredirect=${escapetool.url($xredirect)}&amp;form_token=$!{services.csrf.getToken()}"))
       {{warning}}
-        $services.localization.render('platform.xclass.defaultClassSheet.template.missingObject', ["//$className//"]) ##
-        {{html}}&lt;a href="$createUrl"&gt;$escapetool.xml($services.localization.render(
-          'platform.xclass.defaultClassSheet.template.addObject', [$className])) »&lt;/a&gt;.{{/html}}
+        #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.template.missingObject', ['__CLASS_NAME__']))
+        #set ($message = $escapetool.xml($message))
+        {{html}}
+          ## First escape the content of the translation, then replace the placeholders with content that would
+          ## otherwise be escaped during the first escaping.
+          $message.replace('__CLASS_NAME__', "&lt;em&gt;$escapetool.xml($className)&lt;/em&gt;")
+          &lt;a href="$escapetool.xml($createUrl)"&gt;##
+            $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.template.addObject', [$className])) »##
+          &lt;/a&gt;.
+        {{/html}}
       {{/warning}}
 
     #end
     #set ($templatePath = "#hierarchy($classTemplateDoc.documentReference, {'plain': true, 'local': true, 'limit': 4})")
-    [[$services.localization.render('platform.xclass.defaultClassSheet.template.view',
-      [$templatePath.trim()]) »&gt;&gt;${classTemplateDoc.fullName}]]
+    #set ($templateDocLink = "$services.localization.render('platform.xclass.defaultClassSheet.template.view', [$templatePath.trim()]) »")
+    #set ($templateDocLink = $services.rendering.escape($templateDocLink, 'xwiki/2.1'))
+    #set ($templateDocLink = $services.rendering.escape($templateDocLink, 'xwiki/2.1'))
+    #set ($templateDocText = "${classTemplateDoc.fullName}")
+    ## First escape the xwiki/2.1 syntax of the translation, then replace the placeholders with content that would
+    ## otherwise be escaped during the first escaping.
+    #set ($templateDocText = $services.rendering.escape($templateDocText, 'xwiki/2.1'))
+    [[$templateDocLink&gt;&gt;$templateDocText]]
   #end
   ## Create a template provider only if a template for the current class exists.
   #if ($classTemplateDoc.getObject(${doc.fullName}))
     (% id="HClassTemplateProvider" %)
     = {{translation key="platform.xclass.defaultClassSheet.templateProvider.heading"/}} =
 
-      {{info}}$services.localization.render('platform.xclass.defaultClassSheet.templateProvider.description',
-        ['//']){{/info}}
+      {{info}}
+        #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.templateProvider.description', ['__EM__']))
+        #set ($message = $services.rendering.escape($message, 'xwiki/2.1'))
+        ## First escape the xwiki/2.1 syntax of the translation, then replace the placeholders with content that would
+        ## otherwise be escaped during the first escaping.
+        ## The replacement key is itself escaped, and it's escaped form needs to be used for the replacement.
+        $message.replace('~_~_~E~M~_~_', '//')
+      {{/info}}
 
     #if (!$hasClassTemplateProvider)
       #set ($templateProviderClassName = 'XWiki.TemplateProviderClass')
       ## Do the page creation and object addition in one step, providing some default values.
       ## In order to get the root space of the class and use it as restrictionSpace, we need to be sure that we have
@@ -360,25 +411,28 @@
           $services.localization.render('platform.xclass.templateProvider.defaultDescription', [$className]),
         "${templateProviderClassName}_template": $classTemplateDoc,
         "${templateProviderClassName}_visibilityRestrictions": $restrictionSpace}))
       #set ($createUrl = $classTemplateProviderDoc.getURL('objectadd', $createUrlQueryString))
       {{html}}
-        &lt;form action="$classTemplateProviderDoc.getURL('save', 'editor=wiki')" method="post"&gt;
+        &lt;form action="$escapetool.xml($classTemplateProviderDoc.getURL('save', 'editor=wiki'))" method="post"&gt;
           &lt;div&gt;
             &lt;input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" /&gt;
-            &lt;input type="hidden" name="parent" value="${doc.fullName}"/&gt;
-            &lt;input type="hidden" name="xredirect" value="$createUrl"/&gt;
-            &lt;input type="hidden" name="title" value="$className Template Provider"/&gt;
+            &lt;input type="hidden" name="parent" value="$escapetool.xml(${doc.fullName})"/&gt;
+            &lt;input type="hidden" name="xredirect" value="$escapetool.xml($createUrl)"/&gt;
+            &lt;input type="hidden" name="title" value="$escapetool.xml($className) Template Provider"/&gt;
             &lt;span class="buttonwrapper"&gt;&lt;input type="submit" class="button" value="$escapetool.xml(
               $services.localization.render('platform.xclass.defaultClassSheet.templateProvider.create'))"/&gt;&lt;/span&gt;
           &lt;/div&gt;
         &lt;/form&gt;
       {{/html}}
     #else
       #set ($templateProviderPath = "#hierarchy($classTemplateProviderDoc.documentReference, {'plain': true, 'local': true, 'limit': 4})")
-      [[$services.localization.render('platform.xclass.defaultClassSheet.templateProvider.view',
-        [$templateProviderPath.trim()]) »&gt;&gt;${classTemplateProviderDoc.fullName}]]
+      #set ($linkTarget = "$services.localization.render('platform.xclass.defaultClassSheet.templateProvider.view', [$templateProviderPath.trim()]) »")
+      #set ($linkTarget = $services.rendering.escape($linkTarget, 'xwiki/2.1'))
+      #set ($linkTarget = $services.rendering.escape($linkTarget, 'xwiki/2.1'))
+      #set ($linkLabel = $services.rendering.escape(${classTemplateProviderDoc.fullName}, 'xwiki/2.1'))
+      [[$linkTarget&gt;&gt;$linkLabel]]
     #end
   #end
 
 #end## !$isSheet
 {{/velocity}}</content>
diff --git a/xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/src/test/java/org/xwiki/xclass/ClassSheetPageTest.java b/xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/src/test/java/org/xwiki/xclass/ClassSheetPageTest.java
new file mode 100644
index 0000000000000..0c5de4a59c23f
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-xclass/xwiki-platform-xclass-ui/src/test/java/org/xwiki/xclass/ClassSheetPageTest.java
@@ -0,0 +1,460 @@
+/*
+ * 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.xclass;
+
+import java.util.HashMap;
+
+import javax.script.ScriptContext;
+
+import org.jsoup.Jsoup;
+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.api.Test;
+import org.xwiki.icon.IconManager;
+import org.xwiki.livedata.internal.macro.LiveDataMacroComponentList;
+import org.xwiki.localization.macro.internal.TranslationMacro;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.script.ModelScriptService;
+import org.xwiki.rendering.RenderingScriptServiceComponentList;
+import org.xwiki.rendering.internal.configuration.DefaultRenderingConfigurationComponentList;
+import org.xwiki.rendering.internal.macro.message.InfoMessageMacro;
+import org.xwiki.rendering.internal.macro.message.WarningMessageMacro;
+import org.xwiki.script.ScriptContextManager;
+import org.xwiki.template.internal.macro.TemplateMacro;
+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.TestNoScriptMacro;
+import org.xwiki.test.page.XWikiSyntax20ComponentList;
+import org.xwiki.test.page.XWikiSyntax21ComponentList;
+import org.xwiki.velocity.tools.EscapeTool;
+
+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 com.xpn.xwiki.plugin.skinx.SkinExtensionPluginApi;
+import com.xpn.xwiki.script.sheet.SheetScriptService;
+
+import static javax.script.ScriptContext.GLOBAL_SCOPE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Page Test of {@code XWiki.ClassSheet}.
+ *
+ * @version $Id$
+ * @since 15.0RC1
+ * @since 14.10.3
+ * @since 14.4.8
+ */
+@HTML50ComponentList
+@XWikiSyntax21ComponentList
+@XWikiSyntax20ComponentList
+@LiveDataMacroComponentList
+@RenderingScriptServiceComponentList
+@DefaultRenderingConfigurationComponentList
+@ComponentList({
+    TemplateMacro.class,
+    TranslationMacro.class,
+    WarningMessageMacro.class,
+    InfoMessageMacro.class,
+    TemplateScriptService.class,
+    TestNoScriptMacro.class,
+    ModelScriptService.class,
+    SheetScriptService.class
+})
+class ClassSheetPageTest extends PageTest
+{
+    private static final DocumentReference CLASS_SHEET_BINDING_DOCUMENT_REFERENCE =
+        new DocumentReference("xwiki", "XWiki", "ClassSheetBinding");
+
+    private static final DocumentReference XWIKI_CLASS_SHEET = new DocumentReference("xwiki", "XWiki", "ClassSheet");
+
+    private ScriptContext scriptContext;
+
+    @BeforeEach
+    void setUp() throws Exception
+    {
+        // Spy the jsfx plugin used during the macro rendering to return a mock of its API when required. 
+        when(this.oldcore.getSpyXWiki().getPluginApi("jsfx", this.context))
+            .thenReturn(mock(SkinExtensionPluginApi.class));
+
+        // Return minimal icons metadata since this is not what we want to test in this test suite.
+        IconManager iconManager = this.componentManager.registerMockComponent(IconManager.class);
+        doReturn(new HashMap<>()).when(iconManager).getMetaData(anyString());
+
+        initializeClassSheetBinding();
+
+        this.scriptContext = this.oldcore.getMocker().<ScriptContextManager>getInstance(ScriptContextManager.class)
+            .getCurrentScriptContext();
+    }
+
+    @Test
+    void notASheetNoProperties() throws Exception
+    {
+        XWikiDocument xwikiDocument =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "/}}{{/html}}{{noscript/}}"), this.context);
+        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);
+
+        // Set up the current doc in the context so that $doc is bound in scripts
+        this.context.setDoc(xwikiDocument);
+
+        Document document = render(doc);
+
+        Elements forms = document.select("form");
+
+        Element warningMessage = document.selectFirst(".warningmessage");
+        assertEquals("platform.xclass.defaultClassSheet.properties.empty [, ]", warningMessage.text());
+        assertEquals("/xwiki/bin/edit/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D?editor=class",
+            warningMessage.selectFirst("a").attr("href"));
+        Element classSheetForm = forms.get(0);
+        assertEquals("Space./}}{{/html}}{{noscript/}}", classSheetForm.selectFirst("[name='parent']").val());
+        assertEquals("/xwiki/bin/view/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D",
+            classSheetForm.selectFirst("[name='xredirect']").val());
+        assertEquals("#if($doc.documentReference.name == '/}}{{/html}}{{noscript/}}Sheet')/}}{{/html}}"
+            + "{{noscript/}} Sheet#{else}$services.display.title($doc, {'displayerHint': 'default', "
+            + "'outputSyntaxId': 'plain/1.0'})#end", classSheetForm.selectFirst("[name='title']").val());
+        Element classTemplateForm = forms.get(1);
+        assertEquals("Space./}}{{/html}}{{noscript/}}", classTemplateForm.selectFirst("[name='parent']").val());
+        assertEquals("/xwiki/bin/view/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D",
+            classTemplateForm.selectFirst("[name='xredirect']").val());
+        assertEquals("/}}{{/html}}{{noscript/}} Template", classTemplateForm.selectFirst("[name='title']").val());
+        Elements infomessages = document.select(".infomessage");
+        Element infomessage1 = infomessages.get(0);
+        assertEquals("platform.xclass.defaultClassSheet.sheets.description [, ]", infomessage1.text());
+        assertNotNull(infomessage1.selectFirst("em"));
+        Element infomessage2 = infomessages.get(1);
+        assertEquals("platform.xclass.defaultClassSheet.template.description [, ]", infomessage2.text());
+        assertNotNull(infomessage2.selectFirst("em"));
+    }
+
+    @Test
+    void notASheetHasProperties() throws Exception
+    {
+        XWikiDocument xwikiDocument =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "/}}{{/html}}{{noscript/}}"), this.context);
+        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
+        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);
+
+        // Set up the current doc in the context so that $doc is bound in scripts
+        this.context.setDoc(xwikiDocument);
+
+        Document document = render(doc);
+        Elements forms = document.select("form");
+
+        Element ul = document.selectFirst("ul");
+        Elements lis = ul.select("li");
+        assertEquals("Test Field (testField: String)", lis.get(0).text());
+        assertEquals("platform.xclass.defaultClassSheet.properties.edit [, ]", lis.get(1).text());
+        assertEquals("/xwiki/bin/edit/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D?editor=class",
+            lis.get(1).selectFirst("a").attr("href"));
+        Element classSheetForm = forms.get(0);
+        assertEquals("Space./}}{{/html}}{{noscript/}}", classSheetForm.selectFirst("[name='parent']").val());
+        assertEquals("/xwiki/bin/view/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D",
+            classSheetForm.selectFirst("[name='xredirect']").val());
+        assertEquals("#if($doc.documentReference.name == '/}}{{/html}}{{noscript/}}Sheet')/}}{{/html}}"
+            + "{{noscript/}} Sheet#{else}$services.display.title($doc, {'displayerHint': 'default', "
+            + "'outputSyntaxId': 'plain/1.0'})#end", classSheetForm.selectFirst("[name='title']").val());
+        Element classTemplateForm = forms.get(1);
+        assertEquals("Space./}}{{/html}}{{noscript/}}", classTemplateForm.selectFirst("[name='parent']").val());
+        assertEquals("/xwiki/bin/view/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D",
+            classTemplateForm.selectFirst("[name='xredirect']").val());
+        assertEquals("/}}{{/html}}{{noscript/}} Template", classTemplateForm.selectFirst("[name='title']").val());
+    }
+
+    @Test
+    void notASheetHasSheetAlreadyExists() throws Exception
+    {
+        String alreadyExistsPageName = "AlreadyExists/}}{{noscript/}}";
+        XWikiDocument alreadyExistsDoc =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", alreadyExistsPageName), this.context);
+        this.xwiki.saveDocument(alreadyExistsDoc, this.context);
+
+        XWikiDocument xwikiDocument =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "/}}{{/html}}{{noscript/}}"), this.context);
+        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
+        BaseObject baseObject = xwikiDocument.newXObject(CLASS_SHEET_BINDING_DOCUMENT_REFERENCE, this.context);
+        baseObject.set("sheet", alreadyExistsPageName, this.context);
+        this.xwiki.saveDocument(xwikiDocument, this.context);
+        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);
+
+        // Set up the current doc in the context so that $doc is bound in scripts
+        this.context.setDoc(xwikiDocument);
+
+        Document document = render(doc);
+
+        Elements h1s = document.select("h1");
+        assertEquals("platform.xclass.defaultClassSheet.properties.heading", h1s.get(0).text());
+        assertEquals("platform.xclass.defaultClassSheet.pages.heading", h1s.get(1).text());
+
+        Element ul = document.selectFirst("ul");
+        Elements lis = ul.select("li");
+        assertEquals("Test Field (testField: String)", lis.get(0).text());
+        assertEquals("platform.xclass.defaultClassSheet.properties.edit [, ]", lis.get(1).text());
+        assertEquals("/xwiki/bin/edit/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D?editor=class",
+            lis.get(1).selectFirst("a").attr("href"));
+        Element classSheetForm = document.selectFirst("form");
+        assertEquals("Space./}}{{/html}}{{noscript/}}", classSheetForm.selectFirst("[name='parent']").val());
+        assertEquals("/xwiki/bin/view/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D",
+            classSheetForm.selectFirst("[name='xredirect']").val());
+        assertEquals("/}}{{/html}}{{noscript/}} Template", classSheetForm.selectFirst("[name='title']").val());
+
+        assertEquals(String.format("platform.xclass.defaultClassSheet.sheets.view [Space / %s] »",
+                new EscapeTool().xml(alreadyExistsPageName)),
+            document.selectFirst(String.format("[href='Space.%s']", alreadyExistsPageName)).text());
+    }
+
+    @Test
+    void notASheetHasSheetAlreadyExistsClassTemplateExists() throws Exception
+    {
+        String alreadyExistsPageName = "AlreadyExists/}}{{noscript/}}";
+        String pageName = "/}}{{/html}}{{noscript/}}";
+        XWikiDocument alreadyExistsDoc =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", alreadyExistsPageName), this.context);
+        this.xwiki.saveDocument(alreadyExistsDoc, this.context);
+
+        XWikiDocument alreadyExistsDocTemplate =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", pageName + "Template"), this.context);
+        this.xwiki.saveDocument(alreadyExistsDocTemplate, this.context);
+
+        XWikiDocument xwikiDocument =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", pageName), this.context);
+        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
+        BaseObject baseObject = xwikiDocument.newXObject(CLASS_SHEET_BINDING_DOCUMENT_REFERENCE, this.context);
+        baseObject.set("sheet", alreadyExistsPageName, this.context);
+        this.xwiki.saveDocument(xwikiDocument, this.context);
+        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);
+
+        // Set up the current doc in the context so that $doc is bound in scripts
+        this.context.setDoc(xwikiDocument);
+
+        Document document = render(doc);
+
+        Elements h1s = document.select("h1");
+        assertEquals("platform.xclass.defaultClassSheet.properties.heading", h1s.get(0).text());
+        assertEquals("platform.xclass.defaultClassSheet.createPage.heading", h1s.get(1).text());
+
+        Element ul = document.selectFirst("ul");
+        Elements lis = ul.select("li");
+        assertEquals("Test Field (testField: String)", lis.get(0).text());
+        assertEquals(String.format("%s [, ]", "platform.xclass.defaultClassSheet.properties.edit"), lis.get(1).text());
+        assertEquals("/xwiki/bin/edit/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D?editor=class",
+            lis.get(1).selectFirst("a").attr("href"));
+        Element classSheetForm = document.selectFirst("form");
+        assertEquals("Space./}}{{/html}}{{noscript/}}", classSheetForm.selectFirst("[name='parent']").val());
+        assertEquals("Space./}}{{/html}}{{noscript/}}Template", classSheetForm.selectFirst("[name='template']").val());
+        assertEquals("1", classSheetForm.selectFirst("[name='sheet']").val());
+
+        assertEquals(String.format("platform.xclass.defaultClassSheet.sheets.view [Space / %s] »",
+                new EscapeTool().xml(alreadyExistsPageName)),
+            document.selectFirst(String.format("[href='Space.%s']", alreadyExistsPageName)).text());
+    }
+
+    @Test
+    void hasClassSheetsAndHasClassTemplate() throws Exception
+    {
+        String pageName = "Page";
+        String sheetPage = "MySheet";
+        DocumentReference mainPageReference = new DocumentReference("xwiki", "Space", pageName);
+        XWikiDocument xwikiDocument = this.xwiki.getDocument(mainPageReference, this.context);
+        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
+        BaseObject classSheetBindingObject =
+            xwikiDocument.newXObject(CLASS_SHEET_BINDING_DOCUMENT_REFERENCE, this.context);
+        classSheetBindingObject.set("sheet", sheetPage, this.context);
+
+        this.xwiki.saveDocument(xwikiDocument, this.context);
+        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);
+
+        // Class Template Creation.
+        XWikiDocument templateProvider = this.xwiki.getDocument(
+            new DocumentReference(pageName + "Template", mainPageReference.getLastSpaceReference()),
+            this.context);
+        this.xwiki.saveDocument(templateProvider, this.context);
+
+        // Class docRef
+        XWikiDocument docRef = this.xwiki.getDocument(
+            new DocumentReference("DOC_NAME", mainPageReference.getLastSpaceReference()),
+            this.context);
+        this.xwiki.saveDocument(docRef, this.context);
+
+        this.request.put("docName", "DOC_NAME");
+
+        // Set up the current doc in the context so that $doc is bound in scripts
+        this.context.setDoc(xwikiDocument);
+
+        Document document = render(doc);
+
+        Elements warnings = document.select(".warningmessage");
+        Element warning1 = warnings.get(0);
+        assertEquals("platform.xclass.defaultClassSheet.createPage.pageAlreadyExists [, ]", warning1.text());
+        assertEquals("/xwiki/bin/view/Space/DOC_NAME", warning1.selectFirst("a").attr("href"));
+        Element warning2 = warnings.get(1);
+        assertEquals("platform.xclass.defaultClassSheet.template.missingObject [Page] "
+            + "platform.xclass.defaultClassSheet.template.addObject [Page] ».", warning2.text());
+        assertEquals("/xwiki/bin/objectadd/Space/PageTemplate"
+                + "?classname=Space.Page&xredirect=%2Fxwiki%2Fbin%2FMain%2FWebHome&form_token=",
+            warning2.selectFirst("a").attr("href"));
+    }
+
+    @Test
+    void hasEditAndDefaultClassSheetReference() throws Exception
+    {
+        this.scriptContext.setAttribute("hasEdit", true, GLOBAL_SCOPE);
+
+        String pageName = "Page";
+        DocumentReference mainPageReference = new DocumentReference("xwiki", "Space", pageName);
+        XWikiDocument xwikiDocument = this.xwiki.getDocument(mainPageReference, this.context);
+        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
+        this.xwiki.saveDocument(xwikiDocument, this.context);
+
+        // Create defaultClassSheetReference
+        XWikiDocument defaultClassSheet =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageSheet"), this.context);
+        this.xwiki.saveDocument(defaultClassSheet, this.context);
+
+        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);
+
+        this.request.put("docName", "DOC_NAME");
+
+        // Set up the current doc in the context so that $doc is bound in scripts
+        this.context.setDoc(xwikiDocument);
+
+        Document document = render(doc);
+
+        Element warning = document.selectFirst(".warningmessage");
+        assertEquals("platform.xclass.defaultClassSheet.sheets.notBound "
+            + "platform.xclass.defaultClassSheet.sheets.bind ».", warning.text());
+        assertEquals("/xwiki/bin/view/Space/Page?bindSheet=xwiki%3ASpace.PageSheet"
+            + "&xredirect=%2Fxwiki%2Fbin%2FMain%2FWebHome&form_token=", warning.selectFirst("a").attr("href"));
+    }
+
+    @Test
+    void hasEditAndDefaultClassSheetReferenceTemplateHasDoc() throws Exception
+    {
+        this.scriptContext.setAttribute("hasEdit", true, GLOBAL_SCOPE);
+
+        String pageName = "Page";
+        DocumentReference mainPageReference = new DocumentReference("xwiki", "Space", pageName);
+        XWikiDocument xwikiDocument = this.xwiki.getDocument(mainPageReference, this.context);
+        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
+        this.xwiki.saveDocument(xwikiDocument, this.context);
+
+        // Create defaultClassSheetReference
+        XWikiDocument defaultClassSheet =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageSheet"), this.context);
+        this.xwiki.saveDocument(defaultClassSheet, this.context);
+
+        XWikiDocument pageTemplate =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageTemplate"), this.context);
+        pageTemplate.newXObject(mainPageReference, this.context);
+        this.xwiki.saveDocument(pageTemplate, this.context);
+
+        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);
+
+        this.request.put("docName", "DOC_NAME");
+
+        // Set up the current doc in the context so that $doc is bound in scripts
+        this.context.setDoc(xwikiDocument);
+
+        Document document = render(doc);
+
+        Elements infomessages = document.select(".infomessage");
+        assertEquals("platform.xclass.defaultClassSheet.sheets.description [, ]", infomessages.get(0).text());
+        assertEquals("platform.xclass.defaultClassSheet.template.description [, ]", infomessages.get(1).text());
+        assertEquals("platform.xclass.defaultClassSheet.templateProvider.description []", infomessages.get(2).text());
+        assertNotNull(infomessages.get(2).selectFirst("em"));
+    }
+
+    @Test
+    void hasEditAndDefaultClassSheetReferenceTemplateHasDocHasClassTemplateProvider() throws Exception
+    {
+        this.scriptContext.setAttribute("hasEdit", true, GLOBAL_SCOPE);
+
+        DocumentReference mainPageReference = new DocumentReference("xwiki", "Space", "Page");
+        XWikiDocument xwikiDocument = this.xwiki.getDocument(mainPageReference, this.context);
+        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
+        this.xwiki.saveDocument(xwikiDocument, this.context);
+
+        // Create defaultClassSheetReference
+        XWikiDocument defaultClassSheet =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageSheet"), this.context);
+        this.xwiki.saveDocument(defaultClassSheet, this.context);
+
+        XWikiDocument pageTemplate =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageTemplate"), this.context);
+        pageTemplate.newXObject(mainPageReference, this.context);
+        this.xwiki.saveDocument(pageTemplate, this.context);
+
+        XWikiDocument pageTemplateProvider =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageTemplateProvider"), this.context);
+        this.xwiki.saveDocument(pageTemplateProvider, this.context);
+
+        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);
+
+        this.request.put("docName", "DOC_NAME");
+
+        // Set up the current doc in the context so that $doc is bound in scripts
+        this.context.setDoc(xwikiDocument);
+
+        Document document = render(doc);
+
+        Elements infomessages = document.select(".infomessage");
+        assertEquals("platform.xclass.defaultClassSheet.sheets.description [, ]", infomessages.get(0).text());
+        assertEquals("platform.xclass.defaultClassSheet.template.description [, ]", infomessages.get(1).text());
+        assertEquals("platform.xclass.defaultClassSheet.templateProvider.description []", infomessages.get(2).text());
+        assertNotNull(infomessages.get(2).selectFirst("em"));
+        Elements links = document.select("a");
+        Element lastLink = links.get(links.size() - 1);
+        assertEquals("platform.xclass.defaultClassSheet.templateProvider.view [Space / PageTemplateProvider] »",
+            lastLink.text());
+        assertEquals("Space.PageTemplateProvider", lastLink.attr("href"));
+    }
+
+    private Document render(XWikiDocument doc) throws XWikiException
+    {
+        return Jsoup.parse(doc.getRenderedContent(this.context));
+    }
+
+    /**
+     * TODO: Create the class because it is required but we currently do not support loading documents from other
+     * modules. An option would be to move ClassSheetBinding to a mandatory document initializer.
+     */
+    private void initializeClassSheetBinding() throws Exception
+    {
+        XWikiDocument document = this.xwiki.getDocument(CLASS_SHEET_BINDING_DOCUMENT_REFERENCE, this.context);
+
+        if (!document.isNew()) {
+            return;
+        }
+
+        BaseClass xClass = document.getXClass();
+        xClass.addPageField("sheet", "Sheet", 30, false, false, "");
+
+        this.xwiki.saveDocument(document, this.context);
+    }
+}

Vulnerable code:         <label for="$!{options.id}Title">$services.localization.render($titleField.label)</label>
        <span class="xHint">$!services.localization.render($titleField.hint)</span>
        <input type="text" id="$!{options.id}Title" name="$titleField.name" value="$!escapedValue"
          class="location-title-field" placeholder="$!services.localization.render($titleField.placeholder)" />
      <label>$services.localization.render($options.preview.label)</label>
      <span class="xHint">$services.localization.render($options.preview.hint)</span>
          <label for="$!{options.id}Wiki">$services.localization.render($wikiField.label)</label>
          <span class="xHint">$!services.localization.render($wikiField.hint)</span>
        <label for="$!{options.id}ParentReference">$services.localization.render($parentField.label)</label>
        <span class="xHint">$!services.localization.render($parentField.hint)</span>
        <input type="text" id="$!{options.id}ParentReference" class="location-parent-field suggestSpaces"
          name="$parentField.name" value="$!escapedValue"
          placeholder="$!services.localization.render($parentField.placeholder)" />
          <label for="$!{options.id}Name">$services.localization.render($nameField.label)</label>
          <span class="xHint">$!services.localization.render($nameField.hint)</span>
          <input type="text" id="$!{options.id}Name" name="$nameField.name" class="location-name-field"
            placeholder="$!services.localization.render($nameField.placeholder)" />
    {{warning}}$services.localization.render('platform.xclass.defaultClassSheet.properties.empty', [
      "{{html}}&lt;a href='$classEditorURL'&gt;",
      '&lt;/a&gt;{{/html}}'
    ]){{/warning}}
    * //$services.localization.render('platform.xclass.defaultClassSheet.properties.edit', [
      "{{html}}&lt;a href='$classEditorURL'&gt;",
      '&lt;/a&gt;{{/html}}'
    ])//

      {{warning}}$services.localization.render('platform.xclass.defaultClassSheet.createPage.pageAlreadyExists', [
        '[[',
        "&gt;&gt;$services.model.serialize($targetDocRef)]]"
      ]){{/warning}}
        &lt;input type="hidden" name="parent" value="${defaultParent}"/&gt;
        &lt;input type="hidden" name="template" value="${classTemplateDoc}"/&gt;
    className="${doc.fullName}"
    sourceParameters="${escapetool.url($options)}"
  {{info}}$services.localization.render('platform.xclass.defaultClassSheet.sheets.description', ['//', '//']){{/info}}
          &lt;input type="hidden" name="parent" value="${doc.fullName}"/&gt;
          &lt;input type="hidden" name="xredirect" value="${doc.URL}"/&gt;
          &lt;a href="$bindURL"&gt;$services.localization.render('platform.xclass.defaultClassSheet.sheets.bind') »&lt;/a&gt;.
      [[$services.localization.render('platform.xclass.defaultClassSheet.sheets.view', [$sheetPath.trim()]) »&gt;&gt;${classSheetDoc.fullName}]]
    {{info}}$services.localization.render('platform.xclass.defaultClassSheet.template.description',
      ['//', '//']){{/info}}
      &lt;form action="$classTemplateDoc.getURL('save', 'editor=wiki')" method="post"&gt;
          &lt;input type="hidden" name="parent" value="${doc.fullName}"/&gt;
          &lt;input type="hidden" name="xredirect" value="${doc.URL}"/&gt;
          &lt;input type="hidden" name="title" value="$className Template"/&gt;
      #set($createUrl = $classTemplateDoc.getURL('objectadd', "classname=${escapetool.url($doc.fullName)}&amp;amp;xredirect=${escapetool.url($xredirect)}&amp;amp;form_token=$!{services.csrf.getToken()}"))
        $services.localization.render('platform.xclass.defaultClassSheet.template.missingObject', ["//$className//"]) ##
        {{html}}&lt;a href="$createUrl"&gt;$escapetool.xml($services.localization.render(
          'platform.xclass.defaultClassSheet.template.addObject', [$className])) »&lt;/a&gt;.{{/html}}
    [[$services.localization.render('platform.xclass.defaultClassSheet.template.view',
      [$templatePath.trim()]) »&gt;&gt;${classTemplateDoc.fullName}]]
      {{info}}$services.localization.render('platform.xclass.defaultClassSheet.templateProvider.description',
        ['//']){{/info}}
        &lt;form action="$classTemplateProviderDoc.getURL('save', 'editor=wiki')" method="post"&gt;
            &lt;input type="hidden" name="parent" value="${doc.fullName}"/&gt;
            &lt;input type="hidden" name="xredirect" value="$createUrl"/&gt;
            &lt;input type="hidden" name="title" value="$className Template Provider"/&gt;
      [[$services.localization.render('platform.xclass.defaultClassSheet.templateProvider.view',
        [$templateProviderPath.trim()]) »&gt;&gt;${classTemplateProviderDoc.fullName}]]
Fixed code:         <label for="$escapetool.xml($!{options.id})Title">##
          $escapetool.xml($services.localization.render($titleField.label))##
        </label>
        <span class="xHint">$!escapetool.xml($services.localization.render($titleField.hint))</span>
        <input type="text" id="$escapetool.xml($!{options.id})Title" name="$escapetool.xml($titleField.name)" value="$!escapedValue"
          class="location-title-field" placeholder="$escapetool.xml($!services.localization.render($titleField.placeholder))" />
      <label>$escapetool.xml($services.localization.render($options.preview.label))</label>
      <span class="xHint">$escapetool.xml($services.localization.render($options.preview.hint))</span>
          <label for="$escapetool.xml($!{options.id})Wiki">##
            $escapetool.xml($services.localization.render($wikiField.label))##
          </label>
          <span class="xHint">$!escapetool.xml($services.localization.render($wikiField.hint))</span>
        <label for="$escapetool.xml($!{options.id})ParentReference">##
          $escapetool.xml($services.localization.render($parentField.label))##
        </label>
        <span class="xHint">$!escapetool.xml($services.localization.render($parentField.hint))</span>
        <input type="text" id="$escapetool.xml($!{options.id})ParentReference" class="location-parent-field suggestSpaces"
          name="$escapetool.xml($parentField.name)" value="$!escapedValue"
          placeholder="$!escapetool.xml($services.localization.render($parentField.placeholder))" />
          <label for="$escapetool.xml($!{options.id})Name">##
            $escapetool.xml($services.localization.render($nameField.label))##
          </label>
          <span class="xHint">$escapetool.xml($services.localization.render($nameField.hint))</span>
          <input type="text" id="$escapetool.xml($!{options.id})Name" name="$escapetool.xml($nameField.name)" class="location-name-field"
            placeholder="$escapetool.xml($!services.localization.render($nameField.placeholder))" />
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-rendering-xwiki</artifactId>
      <version>${project.version}</version>
      <scope>runtime</scope>
    </dependency>
    <!-- Test dependencies. -->
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-test-page</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-web-templates</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-livedata-macro</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
      <type>test-jar</type>
    </dependency>
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-rendering-xwiki</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
      <type>test-jar</type>
    </dependency>
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-rendering-configuration-default</artifactId>
      <version>${project.version}</version>
      <type>test-jar</type>
      <scope>test</scope>
    </dependency>
    #set ($openLink = "&lt;a href='$escapetool.xml($classEditorURL)'&gt;")
    #set ($closeLink = '&lt;/a&gt;')
    {{warning}}
      {{html}}
      ## First escape the content of the translation, then replace the placeholders with content that would otherwise be
      ## escaped during the first escaping.
      #set ($warningMessage = $services.localization.render('platform.xclass.defaultClassSheet.properties.empty', 
        ['__OPEN_LINK__', '__CLOSE_LINK__']))
      $escapetool.xml($warningMessage).replace('__OPEN_LINK__', $openLink).replace('__CLOSE_LINK__', $closeLink)
      {{/html}}
    {{/warning}}
    #set ($openLink = "&lt;a href='$escapetool.xml($classEditorURL)'&gt;")
    #set ($closeLink = '&lt;/a&gt;')
    #set ($warningMessage = $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.properties.edit', ['__OPEN_LINK__', '__CLOSE_LINK__'])))
    ## First escape the content of the translation, then replace the placeholders with content that would otherwise be
    ## escaped during the first escaping.
    * //{{html}}$warningMessage.replace('__OPEN_LINK__', $openLink).replace('__CLOSE_LINK__', $closeLink){{/html}}//
      {{warning}}
        {{html}}
          #set ($targetDocLink = $xwiki.getURL($targetDocRef))
          #set ($openLink = "&lt;a href='$escapetool.xml($targetDocLink)'&gt;")
          #set ($message = $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.createPage.pageAlreadyExists', ['__OPEN_LINK__', '__CLOSE_LINK__'])))
          ## First escape the content of the translation, then replace the placeholders with content that would
          ## otherwise be escaped during the first escaping.
          $message.replace('__OPEN_LINK__', $openLink).replace('__CLOSE_LINK__', '&lt;/a&gt;')
        {{/html}}
      {{/warning}}
        &lt;input type="hidden" name="parent" value="$escapetool.xml(${defaultParent})"/&gt;
        &lt;input type="hidden" name="template" value="$escapetool.xml(${classTemplateDoc})"/&gt;
    className="$services.rendering.escape(${doc.fullName}, 'xwiki/2.1')"
    sourceParameters="$services.rendering.escape($escapetool.url($options), 'xwiki/2.1')"
  {{info}}
    #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.sheets.description', ['__START_EM__', '__END_EM__']))
    #set ($message = $escapetool.xml($message))
    ## First escape the content of the translation, then replace the placeholders with content that would
    ## otherwise be escaped during the first escaping.
    {{html}}$message.replace('__START_EM__', '&lt;em&gt;').replace('__END_EM__', '&lt;/em&gt;'){{/html}}
  {{/info}}
          &lt;input type="hidden" name="parent" value="$escapetool.xml(${doc.fullName})"/&gt;
          &lt;input type="hidden" name="xredirect" value="$escapetool.xml(${doc.URL})"/&gt;
          &lt;a href="$escapetool.xml($bindURL)"&gt;##
            $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.sheets.bind')) »##
          &lt;/a&gt;.
      #set ($classSheetLink = "$services.localization.render('platform.xclass.defaultClassSheet.sheets.view', [$sheetPath.trim()]) »")
      #set ($classSheetLink = $services.rendering.escape($classSheetLink, 'xwiki/2.1'))
      #set ($classSheetLink = $services.rendering.escape($classSheetLink, 'xwiki/2.1'))
      #set ($classSheetText = ${classSheetDoc.fullName})
      #set ($classSheetText = $services.rendering.escape($classSheetText, 'xwiki/2.1'))
      [[$classSheetLink&gt;&gt;$classSheetText]]
    {{info}}
      #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.template.description', ['__START_EM__', '__END_EM__']))
      #set ($message = $escapetool.xml($message))
      ## First escape the content of the translation, then replace the placeholders with content that would
      ## otherwise be escaped during the first escaping.
      {{html}}$message.replace('__START_EM__', '&lt;em&gt;').replace('__END_EM__', '&lt;/em&gt;'){{/html}}
    {{/info}}
      &lt;form action="$escapetool.xml($classTemplateDoc.getURL('save', 'editor=wiki'))" method="post"&gt;
          &lt;input type="hidden" name="parent" value="$escapetool.xml(${doc.fullName})"/&gt;
          &lt;input type="hidden" name="xredirect" value="$escapetool.xml(${doc.URL})"/&gt;
          &lt;input type="hidden" name="title" value="$escapetool.xml($className) Template"/&gt;
      #set($createUrl = $classTemplateDoc.getURL('objectadd', "classname=${escapetool.url($doc.fullName)}&amp;xredirect=${escapetool.url($xredirect)}&amp;form_token=$!{services.csrf.getToken()}"))
        #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.template.missingObject', ['__CLASS_NAME__']))
        #set ($message = $escapetool.xml($message))
        {{html}}
          ## First escape the content of the translation, then replace the placeholders with content that would
          ## otherwise be escaped during the first escaping.
          $message.replace('__CLASS_NAME__', "&lt;em&gt;$escapetool.xml($className)&lt;/em&gt;")
          &lt;a href="$escapetool.xml($createUrl)"&gt;##
            $escapetool.xml($services.localization.render('platform.xclass.defaultClassSheet.template.addObject', [$className])) »##
          &lt;/a&gt;.
        {{/html}}
    #set ($templateDocLink = "$services.localization.render('platform.xclass.defaultClassSheet.template.view', [$templatePath.trim()]) »")
    #set ($templateDocLink = $services.rendering.escape($templateDocLink, 'xwiki/2.1'))
    #set ($templateDocLink = $services.rendering.escape($templateDocLink, 'xwiki/2.1'))
    #set ($templateDocText = "${classTemplateDoc.fullName}")
    ## First escape the xwiki/2.1 syntax of the translation, then replace the placeholders with content that would
    ## otherwise be escaped during the first escaping.
    #set ($templateDocText = $services.rendering.escape($templateDocText, 'xwiki/2.1'))
    [[$templateDocLink&gt;&gt;$templateDocText]]
      {{info}}
        #set ($message = $services.localization.render('platform.xclass.defaultClassSheet.templateProvider.description', ['__EM__']))
        #set ($message = $services.rendering.escape($message, 'xwiki/2.1'))
        ## First escape the xwiki/2.1 syntax of the translation, then replace the placeholders with content that would
        ## otherwise be escaped during the first escaping.
        ## The replacement key is itself escaped, and it's escaped form needs to be used for the replacement.
        $message.replace('~_~_~E~M~_~_', '//')
      {{/info}}
        &lt;form action="$escapetool.xml($classTemplateProviderDoc.getURL('save', 'editor=wiki'))" method="post"&gt;
            &lt;input type="hidden" name="parent" value="$escapetool.xml(${doc.fullName})"/&gt;
            &lt;input type="hidden" name="xredirect" value="$escapetool.xml($createUrl)"/&gt;
            &lt;input type="hidden" name="title" value="$escapetool.xml($className) Template Provider"/&gt;
      #set ($linkTarget = "$services.localization.render('platform.xclass.defaultClassSheet.templateProvider.view', [$templateProviderPath.trim()]) »")
      #set ($linkTarget = $services.rendering.escape($linkTarget, 'xwiki/2.1'))
      #set ($linkTarget = $services.rendering.escape($linkTarget, 'xwiki/2.1'))
      #set ($linkLabel = $services.rendering.escape(${classTemplateProviderDoc.fullName}, 'xwiki/2.1'))
      [[$linkTarget&gt;&gt;$linkLabel]]
/*
 * 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.xclass;

import java.util.HashMap;

import javax.script.ScriptContext;

import org.jsoup.Jsoup;
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.api.Test;
import org.xwiki.icon.IconManager;
import org.xwiki.livedata.internal.macro.LiveDataMacroComponentList;
import org.xwiki.localization.macro.internal.TranslationMacro;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.script.ModelScriptService;
import org.xwiki.rendering.RenderingScriptServiceComponentList;
import org.xwiki.rendering.internal.configuration.DefaultRenderingConfigurationComponentList;
import org.xwiki.rendering.internal.macro.message.InfoMessageMacro;
import org.xwiki.rendering.internal.macro.message.WarningMessageMacro;
import org.xwiki.script.ScriptContextManager;
import org.xwiki.template.internal.macro.TemplateMacro;
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.TestNoScriptMacro;
import org.xwiki.test.page.XWikiSyntax20ComponentList;
import org.xwiki.test.page.XWikiSyntax21ComponentList;
import org.xwiki.velocity.tools.EscapeTool;

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 com.xpn.xwiki.plugin.skinx.SkinExtensionPluginApi;
import com.xpn.xwiki.script.sheet.SheetScriptService;

import static javax.script.ScriptContext.GLOBAL_SCOPE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Page Test of {@code XWiki.ClassSheet}.
 *
 * @version $Id$
 * @since 15.0RC1
 * @since 14.10.3
 * @since 14.4.8
 */
@HTML50ComponentList
@XWikiSyntax21ComponentList
@XWikiSyntax20ComponentList
@LiveDataMacroComponentList
@RenderingScriptServiceComponentList
@DefaultRenderingConfigurationComponentList
@ComponentList({
    TemplateMacro.class,
    TranslationMacro.class,
    WarningMessageMacro.class,
    InfoMessageMacro.class,
    TemplateScriptService.class,
    TestNoScriptMacro.class,
    ModelScriptService.class,
    SheetScriptService.class
})
class ClassSheetPageTest extends PageTest
{
    private static final DocumentReference CLASS_SHEET_BINDING_DOCUMENT_REFERENCE =
        new DocumentReference("xwiki", "XWiki", "ClassSheetBinding");

    private static final DocumentReference XWIKI_CLASS_SHEET = new DocumentReference("xwiki", "XWiki", "ClassSheet");

    private ScriptContext scriptContext;

    @BeforeEach
    void setUp() throws Exception
    {
        // Spy the jsfx plugin used during the macro rendering to return a mock of its API when required. 
        when(this.oldcore.getSpyXWiki().getPluginApi("jsfx", this.context))
            .thenReturn(mock(SkinExtensionPluginApi.class));

        // Return minimal icons metadata since this is not what we want to test in this test suite.
        IconManager iconManager = this.componentManager.registerMockComponent(IconManager.class);
        doReturn(new HashMap<>()).when(iconManager).getMetaData(anyString());

        initializeClassSheetBinding();

        this.scriptContext = this.oldcore.getMocker().<ScriptContextManager>getInstance(ScriptContextManager.class)
            .getCurrentScriptContext();
    }

    @Test
    void notASheetNoProperties() throws Exception
    {
        XWikiDocument xwikiDocument =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "/}}{{/html}}{{noscript/}}"), this.context);
        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);

        // Set up the current doc in the context so that $doc is bound in scripts
        this.context.setDoc(xwikiDocument);

        Document document = render(doc);

        Elements forms = document.select("form");

        Element warningMessage = document.selectFirst(".warningmessage");
        assertEquals("platform.xclass.defaultClassSheet.properties.empty [, ]", warningMessage.text());
        assertEquals("/xwiki/bin/edit/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D?editor=class",
            warningMessage.selectFirst("a").attr("href"));
        Element classSheetForm = forms.get(0);
        assertEquals("Space./}}{{/html}}{{noscript/}}", classSheetForm.selectFirst("[name='parent']").val());
        assertEquals("/xwiki/bin/view/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D",
            classSheetForm.selectFirst("[name='xredirect']").val());
        assertEquals("#if($doc.documentReference.name == '/}}{{/html}}{{noscript/}}Sheet')/}}{{/html}}"
            + "{{noscript/}} Sheet#{else}$services.display.title($doc, {'displayerHint': 'default', "
            + "'outputSyntaxId': 'plain/1.0'})#end", classSheetForm.selectFirst("[name='title']").val());
        Element classTemplateForm = forms.get(1);
        assertEquals("Space./}}{{/html}}{{noscript/}}", classTemplateForm.selectFirst("[name='parent']").val());
        assertEquals("/xwiki/bin/view/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D",
            classTemplateForm.selectFirst("[name='xredirect']").val());
        assertEquals("/}}{{/html}}{{noscript/}} Template", classTemplateForm.selectFirst("[name='title']").val());
        Elements infomessages = document.select(".infomessage");
        Element infomessage1 = infomessages.get(0);
        assertEquals("platform.xclass.defaultClassSheet.sheets.description [, ]", infomessage1.text());
        assertNotNull(infomessage1.selectFirst("em"));
        Element infomessage2 = infomessages.get(1);
        assertEquals("platform.xclass.defaultClassSheet.template.description [, ]", infomessage2.text());
        assertNotNull(infomessage2.selectFirst("em"));
    }

    @Test
    void notASheetHasProperties() throws Exception
    {
        XWikiDocument xwikiDocument =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "/}}{{/html}}{{noscript/}}"), this.context);
        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);

        // Set up the current doc in the context so that $doc is bound in scripts
        this.context.setDoc(xwikiDocument);

        Document document = render(doc);
        Elements forms = document.select("form");

        Element ul = document.selectFirst("ul");
        Elements lis = ul.select("li");
        assertEquals("Test Field (testField: String)", lis.get(0).text());
        assertEquals("platform.xclass.defaultClassSheet.properties.edit [, ]", lis.get(1).text());
        assertEquals("/xwiki/bin/edit/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D?editor=class",
            lis.get(1).selectFirst("a").attr("href"));
        Element classSheetForm = forms.get(0);
        assertEquals("Space./}}{{/html}}{{noscript/}}", classSheetForm.selectFirst("[name='parent']").val());
        assertEquals("/xwiki/bin/view/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D",
            classSheetForm.selectFirst("[name='xredirect']").val());
        assertEquals("#if($doc.documentReference.name == '/}}{{/html}}{{noscript/}}Sheet')/}}{{/html}}"
            + "{{noscript/}} Sheet#{else}$services.display.title($doc, {'displayerHint': 'default', "
            + "'outputSyntaxId': 'plain/1.0'})#end", classSheetForm.selectFirst("[name='title']").val());
        Element classTemplateForm = forms.get(1);
        assertEquals("Space./}}{{/html}}{{noscript/}}", classTemplateForm.selectFirst("[name='parent']").val());
        assertEquals("/xwiki/bin/view/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D",
            classTemplateForm.selectFirst("[name='xredirect']").val());
        assertEquals("/}}{{/html}}{{noscript/}} Template", classTemplateForm.selectFirst("[name='title']").val());
    }

    @Test
    void notASheetHasSheetAlreadyExists() throws Exception
    {
        String alreadyExistsPageName = "AlreadyExists/}}{{noscript/}}";
        XWikiDocument alreadyExistsDoc =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", alreadyExistsPageName), this.context);
        this.xwiki.saveDocument(alreadyExistsDoc, this.context);

        XWikiDocument xwikiDocument =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "/}}{{/html}}{{noscript/}}"), this.context);
        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
        BaseObject baseObject = xwikiDocument.newXObject(CLASS_SHEET_BINDING_DOCUMENT_REFERENCE, this.context);
        baseObject.set("sheet", alreadyExistsPageName, this.context);
        this.xwiki.saveDocument(xwikiDocument, this.context);
        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);

        // Set up the current doc in the context so that $doc is bound in scripts
        this.context.setDoc(xwikiDocument);

        Document document = render(doc);

        Elements h1s = document.select("h1");
        assertEquals("platform.xclass.defaultClassSheet.properties.heading", h1s.get(0).text());
        assertEquals("platform.xclass.defaultClassSheet.pages.heading", h1s.get(1).text());

        Element ul = document.selectFirst("ul");
        Elements lis = ul.select("li");
        assertEquals("Test Field (testField: String)", lis.get(0).text());
        assertEquals("platform.xclass.defaultClassSheet.properties.edit [, ]", lis.get(1).text());
        assertEquals("/xwiki/bin/edit/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D?editor=class",
            lis.get(1).selectFirst("a").attr("href"));
        Element classSheetForm = document.selectFirst("form");
        assertEquals("Space./}}{{/html}}{{noscript/}}", classSheetForm.selectFirst("[name='parent']").val());
        assertEquals("/xwiki/bin/view/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D",
            classSheetForm.selectFirst("[name='xredirect']").val());
        assertEquals("/}}{{/html}}{{noscript/}} Template", classSheetForm.selectFirst("[name='title']").val());

        assertEquals(String.format("platform.xclass.defaultClassSheet.sheets.view [Space / %s] »",
                new EscapeTool().xml(alreadyExistsPageName)),
            document.selectFirst(String.format("[href='Space.%s']", alreadyExistsPageName)).text());
    }

    @Test
    void notASheetHasSheetAlreadyExistsClassTemplateExists() throws Exception
    {
        String alreadyExistsPageName = "AlreadyExists/}}{{noscript/}}";
        String pageName = "/}}{{/html}}{{noscript/}}";
        XWikiDocument alreadyExistsDoc =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", alreadyExistsPageName), this.context);
        this.xwiki.saveDocument(alreadyExistsDoc, this.context);

        XWikiDocument alreadyExistsDocTemplate =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", pageName + "Template"), this.context);
        this.xwiki.saveDocument(alreadyExistsDocTemplate, this.context);

        XWikiDocument xwikiDocument =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", pageName), this.context);
        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
        BaseObject baseObject = xwikiDocument.newXObject(CLASS_SHEET_BINDING_DOCUMENT_REFERENCE, this.context);
        baseObject.set("sheet", alreadyExistsPageName, this.context);
        this.xwiki.saveDocument(xwikiDocument, this.context);
        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);

        // Set up the current doc in the context so that $doc is bound in scripts
        this.context.setDoc(xwikiDocument);

        Document document = render(doc);

        Elements h1s = document.select("h1");
        assertEquals("platform.xclass.defaultClassSheet.properties.heading", h1s.get(0).text());
        assertEquals("platform.xclass.defaultClassSheet.createPage.heading", h1s.get(1).text());

        Element ul = document.selectFirst("ul");
        Elements lis = ul.select("li");
        assertEquals("Test Field (testField: String)", lis.get(0).text());
        assertEquals(String.format("%s [, ]", "platform.xclass.defaultClassSheet.properties.edit"), lis.get(1).text());
        assertEquals("/xwiki/bin/edit/Space/%2F%7D%7D%7B%7B%2Fhtml%7D%7D%7B%7Bnoscript%2F%7D%7D?editor=class",
            lis.get(1).selectFirst("a").attr("href"));
        Element classSheetForm = document.selectFirst("form");
        assertEquals("Space./}}{{/html}}{{noscript/}}", classSheetForm.selectFirst("[name='parent']").val());
        assertEquals("Space./}}{{/html}}{{noscript/}}Template", classSheetForm.selectFirst("[name='template']").val());
        assertEquals("1", classSheetForm.selectFirst("[name='sheet']").val());

        assertEquals(String.format("platform.xclass.defaultClassSheet.sheets.view [Space / %s] »",
                new EscapeTool().xml(alreadyExistsPageName)),
            document.selectFirst(String.format("[href='Space.%s']", alreadyExistsPageName)).text());
    }

    @Test
    void hasClassSheetsAndHasClassTemplate() throws Exception
    {
        String pageName = "Page";
        String sheetPage = "MySheet";
        DocumentReference mainPageReference = new DocumentReference("xwiki", "Space", pageName);
        XWikiDocument xwikiDocument = this.xwiki.getDocument(mainPageReference, this.context);
        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
        BaseObject classSheetBindingObject =
            xwikiDocument.newXObject(CLASS_SHEET_BINDING_DOCUMENT_REFERENCE, this.context);
        classSheetBindingObject.set("sheet", sheetPage, this.context);

        this.xwiki.saveDocument(xwikiDocument, this.context);
        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);

        // Class Template Creation.
        XWikiDocument templateProvider = this.xwiki.getDocument(
            new DocumentReference(pageName + "Template", mainPageReference.getLastSpaceReference()),
            this.context);
        this.xwiki.saveDocument(templateProvider, this.context);

        // Class docRef
        XWikiDocument docRef = this.xwiki.getDocument(
            new DocumentReference("DOC_NAME", mainPageReference.getLastSpaceReference()),
            this.context);
        this.xwiki.saveDocument(docRef, this.context);

        this.request.put("docName", "DOC_NAME");

        // Set up the current doc in the context so that $doc is bound in scripts
        this.context.setDoc(xwikiDocument);

        Document document = render(doc);

        Elements warnings = document.select(".warningmessage");
        Element warning1 = warnings.get(0);
        assertEquals("platform.xclass.defaultClassSheet.createPage.pageAlreadyExists [, ]", warning1.text());
        assertEquals("/xwiki/bin/view/Space/DOC_NAME", warning1.selectFirst("a").attr("href"));
        Element warning2 = warnings.get(1);
        assertEquals("platform.xclass.defaultClassSheet.template.missingObject [Page] "
            + "platform.xclass.defaultClassSheet.template.addObject [Page] ».", warning2.text());
        assertEquals("/xwiki/bin/objectadd/Space/PageTemplate"
                + "?classname=Space.Page&xredirect=%2Fxwiki%2Fbin%2FMain%2FWebHome&form_token=",
            warning2.selectFirst("a").attr("href"));
    }

    @Test
    void hasEditAndDefaultClassSheetReference() throws Exception
    {
        this.scriptContext.setAttribute("hasEdit", true, GLOBAL_SCOPE);

        String pageName = "Page";
        DocumentReference mainPageReference = new DocumentReference("xwiki", "Space", pageName);
        XWikiDocument xwikiDocument = this.xwiki.getDocument(mainPageReference, this.context);
        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
        this.xwiki.saveDocument(xwikiDocument, this.context);

        // Create defaultClassSheetReference
        XWikiDocument defaultClassSheet =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageSheet"), this.context);
        this.xwiki.saveDocument(defaultClassSheet, this.context);

        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);

        this.request.put("docName", "DOC_NAME");

        // Set up the current doc in the context so that $doc is bound in scripts
        this.context.setDoc(xwikiDocument);

        Document document = render(doc);

        Element warning = document.selectFirst(".warningmessage");
        assertEquals("platform.xclass.defaultClassSheet.sheets.notBound "
            + "platform.xclass.defaultClassSheet.sheets.bind ».", warning.text());
        assertEquals("/xwiki/bin/view/Space/Page?bindSheet=xwiki%3ASpace.PageSheet"
            + "&xredirect=%2Fxwiki%2Fbin%2FMain%2FWebHome&form_token=", warning.selectFirst("a").attr("href"));
    }

    @Test
    void hasEditAndDefaultClassSheetReferenceTemplateHasDoc() throws Exception
    {
        this.scriptContext.setAttribute("hasEdit", true, GLOBAL_SCOPE);

        String pageName = "Page";
        DocumentReference mainPageReference = new DocumentReference("xwiki", "Space", pageName);
        XWikiDocument xwikiDocument = this.xwiki.getDocument(mainPageReference, this.context);
        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
        this.xwiki.saveDocument(xwikiDocument, this.context);

        // Create defaultClassSheetReference
        XWikiDocument defaultClassSheet =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageSheet"), this.context);
        this.xwiki.saveDocument(defaultClassSheet, this.context);

        XWikiDocument pageTemplate =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageTemplate"), this.context);
        pageTemplate.newXObject(mainPageReference, this.context);
        this.xwiki.saveDocument(pageTemplate, this.context);

        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);

        this.request.put("docName", "DOC_NAME");

        // Set up the current doc in the context so that $doc is bound in scripts
        this.context.setDoc(xwikiDocument);

        Document document = render(doc);

        Elements infomessages = document.select(".infomessage");
        assertEquals("platform.xclass.defaultClassSheet.sheets.description [, ]", infomessages.get(0).text());
        assertEquals("platform.xclass.defaultClassSheet.template.description [, ]", infomessages.get(1).text());
        assertEquals("platform.xclass.defaultClassSheet.templateProvider.description []", infomessages.get(2).text());
        assertNotNull(infomessages.get(2).selectFirst("em"));
    }

    @Test
    void hasEditAndDefaultClassSheetReferenceTemplateHasDocHasClassTemplateProvider() throws Exception
    {
        this.scriptContext.setAttribute("hasEdit", true, GLOBAL_SCOPE);

        DocumentReference mainPageReference = new DocumentReference("xwiki", "Space", "Page");
        XWikiDocument xwikiDocument = this.xwiki.getDocument(mainPageReference, this.context);
        xwikiDocument.getXClass().addTextField("testField", "Test Field", 10);
        this.xwiki.saveDocument(xwikiDocument, this.context);

        // Create defaultClassSheetReference
        XWikiDocument defaultClassSheet =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageSheet"), this.context);
        this.xwiki.saveDocument(defaultClassSheet, this.context);

        XWikiDocument pageTemplate =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageTemplate"), this.context);
        pageTemplate.newXObject(mainPageReference, this.context);
        this.xwiki.saveDocument(pageTemplate, this.context);

        XWikiDocument pageTemplateProvider =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "PageTemplateProvider"), this.context);
        this.xwiki.saveDocument(pageTemplateProvider, this.context);

        XWikiDocument doc = loadPage(XWIKI_CLASS_SHEET);

        this.request.put("docName", "DOC_NAME");

        // Set up the current doc in the context so that $doc is bound in scripts
        this.context.setDoc(xwikiDocument);

        Document document = render(doc);

        Elements infomessages = document.select(".infomessage");
        assertEquals("platform.xclass.defaultClassSheet.sheets.description [, ]", infomessages.get(0).text());
        assertEquals("platform.xclass.defaultClassSheet.template.description [, ]", infomessages.get(1).text());
        assertEquals("platform.xclass.defaultClassSheet.templateProvider.description []", infomessages.get(2).text());
        assertNotNull(infomessages.get(2).selectFirst("em"));
        Elements links = document.select("a");
        Element lastLink = links.get(links.size() - 1);
        assertEquals("platform.xclass.defaultClassSheet.templateProvider.view [Space / PageTemplateProvider] »",
            lastLink.text());
        assertEquals("Space.PageTemplateProvider", lastLink.attr("href"));
    }

    private Document render(XWikiDocument doc) throws XWikiException
    {
        return Jsoup.parse(doc.getRenderedContent(this.context));
    }

    /**
     * TODO: Create the class because it is required but we currently do not support loading documents from other
     * modules. An option would be to move ClassSheetBinding to a mandatory document initializer.
     */
    private void initializeClassSheetBinding() throws Exception
    {
        XWikiDocument document = this.xwiki.getDocument(CLASS_SHEET_BINDING_DOCUMENT_REFERENCE, this.context);

        if (!document.isNew()) {
            return;
        }

        BaseClass xClass = document.getXClass();
        xClass.addPageField("sheet", "Sheet", 30, false, false, "");

        this.xwiki.saveDocument(document, this.context);
    }
}