CVE-2023-29522 (XML, published 2023-04-19T00:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-74 \u2014 Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
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.
Repository: https://github.com/xwiki/xwiki-platform
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 message: XWIKI-20456: Improved escaping of XWiki.ClassSheet


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);
    }
}