CVE-2023-29522 (published 2023-04-19T00:15Z)

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.

CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-74
cwe_name: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
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.
extended_description: Software or other automated logic has certain assumptions about what constitutes data and control respectively. It is the lack of verification of these assumptions for user-controlled input that leads to injection problems. Injection problems encompass a wide variety of issues -- all mitigated in very different ways and usually attempted in order to alter the control flow of the process. For this reason, the most effective way to discuss these weaknesses is to note the distinct features that classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed.
         

Fix information:
repo_url: https://github.com/xwiki/xwiki-platform
commit_details: repo_url: https://github.com/xwiki/xwiki-platform
author: Manuel Leduc
author_date: 2023-01-03 17:37:10+01:00
msg: XWIKI-20456: Improved escaping of XWiki.ClassSheet
repository: repo_url: https://github.com/xwiki/xwiki-platform
repo_name: xwiki/xwiki-platform
description: The XWiki platform
repo_language: Java
owner: xwiki
file_changes: filename: locationPicker_macros.vm
change_type: ModificationType.MODIFY
diff: @@ -40,12 +40,14 @@
     #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>
@@ -60,8 +62,8 @@
     ## ---------------------------------------------------------------------------------------------------------
     ##
     <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)
@@ -113,8 +115,10 @@
       #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">
@@ -156,13 +160,15 @@
       #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>
       ##
       ## ---------------------------------------------------------------------------------------------------------
@@ -177,13 +183,15 @@
       #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>

programming_language: PHP
filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -85,5 +85,45 @@
       <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>

programming_language: HTML
filename: ClassSheet.xml
change_type: ModificationType.MODIFY
diff: @@ -129,31 +129,45 @@
 
   #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}}
@@ -164,8 +178,8 @@
       &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({
@@ -219,8 +233,8 @@
     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": {
@@ -247,15 +261,21 @@
     {{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
@@ -280,7 +300,9 @@
         {{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}}
@@ -292,7 +314,12 @@
         #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"/}}
 
@@ -305,17 +332,22 @@
   (% 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;
@@ -324,25 +356,44 @@
   #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')
@@ -362,12 +413,12 @@
         "${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;
@@ -375,8 +426,11 @@
       {{/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
 

programming_language: CoffeeScript
filename: ClassSheetPageTest.java
change_type: ModificationType.ADD
diff: @@ -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);
+    }
+}

programming_language: Java