CVE-2023-40176: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. Any registered user can exploit a stored XSS through their user profile by setting the payload as the value of the time zone user preference. Even though the time zone is selected from a drop down (no free text value) it can still be set from JavaScript (using the browser developer tools) or by calling the save URL on the user profile with the right query string. Once the time zone is set it is displayed without escaping which means the payload gets executed for any user that visits the malicious user profile, allowing the attacker to steal information and even gain more access rights (escalation to programming rights). This issue is present since version 4.1M2 when the time zone user preference was introduced. The issue has been fixed in XWiki 14.10.5 and 15.1RC1.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2023-08-23T20:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: XML
Files changed: xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/TemplateProviderMacros.xml, xwiki-platform-core/xwiki-platform-annotation/xwiki-platform-annotation-ui/src/main/resources/AnnotationCode/Macros.xml, xwiki-platform-core/xwiki-platform-ckeditor/xwiki-platform-ckeditor-ui/src/main/resources/CKEditor/VelocityMacros.xml, xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/ConfigurationSheet.xml, xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/Sheet.xml, xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml, xwiki-platform-core/xwiki-platform-panels/xwiki-platform-panels-ui/src/main/resources/Panels/PanelWizard.xml, xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-ui/src/main/resources/XWiki/SearchSuggestConfigSheet.xml, xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/pom.xml, xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/src/test/java/org/xwiki/uiextension/script/UIExtensionScriptServiceComponentList.java, xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-directory/xwiki-platform-user-directory-ui/src/main/resources/XWiki/UserDirectoryMacros.xml, xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/pom.xml, xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/src/main/resources/XWiki/XWikiUserPreferencesSheet.xml, xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/src/test/java/org/xwiki/user/profile/UserProfilePageTest.java, xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/displayer_timezone.vm, xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm, xwiki-platform-core/xwiki-platform-wiki/xwiki-platform-wiki-ui/xwiki-platform-wiki-ui-mainwiki/src/main/resources/WikiManager/WikisLiveTableResultsMacros.xml
Commit date: 2023-02-08 10:05:32 +0200
Commit message: XWIKI-20276: Improve escaping in the timezone displayer

Diff: diff --git a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/TemplateProviderMacros.xml b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/TemplateProviderMacros.xml
index 38ec0000b36df..b89a6216edb21 100644
--- a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/TemplateProviderMacros.xml
+++ b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/TemplateProviderMacros.xml
@@ -45,13 +45,11 @@
   #if ($action)
     #set ($output = $doc.display($property, $action))
   #else
     #set ($output = $doc.display($property))
   #end
-  #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
-  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
-  $output
+  #unwrapXPropertyDisplay($output)
 #end
 
 #macro (displayPathsPropertyValue $property)
   &lt;ul class="paths"&gt;
     #set ($hierarchyOptions = {
diff --git a/xwiki-platform-core/xwiki-platform-annotation/xwiki-platform-annotation-ui/src/main/resources/AnnotationCode/Macros.xml b/xwiki-platform-core/xwiki-platform-annotation/xwiki-platform-annotation-ui/src/main/resources/AnnotationCode/Macros.xml
index 4a5521114b4e7..7a206f134a870 100644
--- a/xwiki-platform-core/xwiki-platform-annotation/xwiki-platform-annotation-ui/src/main/resources/AnnotationCode/Macros.xml
+++ b/xwiki-platform-core/xwiki-platform-annotation/xwiki-platform-annotation-ui/src/main/resources/AnnotationCode/Macros.xml
@@ -362,12 +362,10 @@
   #else
     ## We use display('view') and not displayView because we want the property value to be evaluated in the context of
     ## the annotated document (i.e. isolated), not the current document.
     #set ($output = $annotatedDocument.display($annotationProperty.name, 'view', $annotationObject))
     ## Remove the HTML macro wrapping because the output is later injected in another HTML macro with wiki=false.
-    #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
-    #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
-    $output
+    #unwrapXPropertyDisplay($output)
   #end
 #end
 {{/velocity}}</content>
 </xwikidoc>
diff --git a/xwiki-platform-core/xwiki-platform-ckeditor/xwiki-platform-ckeditor-ui/src/main/resources/CKEditor/VelocityMacros.xml b/xwiki-platform-core/xwiki-platform-ckeditor/xwiki-platform-ckeditor-ui/src/main/resources/CKEditor/VelocityMacros.xml
index aee9a06e29010..48f76d476173d 100644
--- a/xwiki-platform-core/xwiki-platform-ckeditor/xwiki-platform-ckeditor-ui/src/main/resources/CKEditor/VelocityMacros.xml
+++ b/xwiki-platform-core/xwiki-platform-ckeditor/xwiki-platform-ckeditor-ui/src/main/resources/CKEditor/VelocityMacros.xml
@@ -239,14 +239,11 @@
   &lt;/dt&gt;
   &lt;dd&gt;&lt;/dd&gt;
 #end
 
 #macro (displayCKEditorConfigPropertyValue $configDoc $propName $action)
-  #set ($output = $configDoc.display($propName, $action))
-  #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
-  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
-  $output
+  #unwrapXPropertyDisplay($configDoc.display($propName, $action))
 #end
 
 #macro (displayCKEditorConfig $configDoc $action)
   #set ($discard = $xwiki.ssx.use('CKEditor.ConfigSheet'))
   #set ($discard = $xwiki.jsx.use('CKEditor.ConfigSheet'))
diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/ConfigurationSheet.xml b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/ConfigurationSheet.xml
index e9d6682008a69..0619a5a162151 100644
--- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/ConfigurationSheet.xml
+++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/ConfigurationSheet.xml
@@ -76,14 +76,11 @@
   #end
   &lt;dd&gt;#displayPDFExportConfigPropertyValue($configProperty, $displayMode)&lt;/dd&gt;
 #end
 
 #macro (displayPDFExportConfigPropertyValue $configProperty $displayMode)
-  #set ($output = $doc.display($configProperty.name, $displayMode))
-  #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
-  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
-  $!output
+  #unwrapXPropertyDisplay($doc.display($configProperty.name, $displayMode))
 #end
 
 #macro (viewPDFExportConfig)
   #foreach ($property in $configClass.properties)
     #displayPDFExportConfigProperty($property.name)
diff --git a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/Sheet.xml b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/Sheet.xml
index 1f9a506d0c8ee..7610f533bb6ac 100644
--- a/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/Sheet.xml
+++ b/xwiki-platform-core/xwiki-platform-export/xwiki-platform-export-pdf/xwiki-platform-export-pdf-ui/src/main/resources/XWiki/PDFExport/Sheet.xml
@@ -103,14 +103,11 @@
     &lt;/body&gt;
   &lt;/html&gt;
 #end
 
 #macro (renderPDFElement $pdfTemplateObj $element)
-  #set ($output = $tdoc.display($element, $pdfTemplateObj))
-  #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
-  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
-  $!output
+  #unwrapXPropertyDisplay($tdoc.display($element, $pdfTemplateObj))
 #end
 
 #macro (renderPDFContent)
   #if ($pdfExportJobId)
     #set ($renderingResults = $services.job.getJobStatus($pdfExportJobId).documentRenderingResults)
diff --git a/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml b/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml
index 622450faaa740..8e858ac6e8d29 100644
--- a/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml
+++ b/xwiki-platform-core/xwiki-platform-livetable/xwiki-platform-livetable-ui/src/main/resources/XWiki/LiveTableResultsMacros.xml
@@ -500,11 +500,11 @@
     #elseif ($fieldProperty.getPropertyClass().classType == 'Email' &amp;&amp; $services.mail.general.shouldObfuscate())
       #set ($fieldValue = $services.mail.general.obfuscate("$!fieldProperty.getValue()"))
     #else
       #set($fieldValue = "$!fieldProperty.getValue()")
     #end
-    #set($fieldDisplayValue = "$!itemDoc.display($colname, 'view')")
+    #set($fieldDisplayValue = "#unwrapXPropertyDisplay($itemDoc.display($colname, 'view'))")
     #if($fieldDisplayValue == '')
       #set($fieldDisplayValue = $services.localization.render("${request.transprefix}emptyvalue"))
     #end
     #set($fieldUrl = '')
     ## Only retrieve an URL for a DBListClass item
@@ -512,11 +512,11 @@
       #set($fieldUrl = $xwiki.getURL($fieldValue))
       #if($fieldUrl == $xwiki.getURL($services.model.resolveDocument('', 'default', $doc.documentReference.extractReference('WIKI'))))
         #set($fieldUrl = '')
       #end
     #end
-    #set($discard = $row.put($colname, $fieldDisplayValue.replaceFirst($regextool.quote('{{html clean="false" wiki="false"}}'), '').replaceAll("$regextool.quote('{{/html}}')$", '')))
+    #set($discard = $row.put($colname, $fieldDisplayValue))
     #set($discard = $row.put("${colname}_value", $fieldValue))
     #set($discard = $row.put("${colname}_url", $fieldUrl))
     ## Reset to the default class
     #set($discard = $itemDoc.use($className))
   #end
diff --git a/xwiki-platform-core/xwiki-platform-panels/xwiki-platform-panels-ui/src/main/resources/Panels/PanelWizard.xml b/xwiki-platform-core/xwiki-platform-panels/xwiki-platform-panels-ui/src/main/resources/Panels/PanelWizard.xml
index 185f410962e9d..58cb7ce6fd369 100644
--- a/xwiki-platform-core/xwiki-platform-panels/xwiki-platform-panels-ui/src/main/resources/Panels/PanelWizard.xml
+++ b/xwiki-platform-core/xwiki-platform-panels/xwiki-platform-panels-ui/src/main/resources/Panels/PanelWizard.xml
@@ -77,14 +77,11 @@
 ##
 {{/velocity}}
 
 {{velocity output="false"}}
 #macro (displayConfigPropertyValue $propName $action)
-  #set ($output = $doc.display($propName, $action))
-  #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
-  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
-  $output
+  #unwrapXPropertyDisplay($doc.display($propName, $action))
 #end
 
 #macro (displayPageLayoutSection)
   &lt;dl&gt;
     &lt;dt&gt;
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-ui/src/main/resources/XWiki/SearchSuggestConfigSheet.xml b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-ui/src/main/resources/XWiki/SearchSuggestConfigSheet.xml
index e416ec1a24fe2..d459a649360dc 100644
--- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-ui/src/main/resources/XWiki/SearchSuggestConfigSheet.xml
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-ui/src/main/resources/XWiki/SearchSuggestConfigSheet.xml
@@ -38,14 +38,11 @@
   <hidden>true</hidden>
   <content>{{include reference="XWiki.SearchCode" /}}
 
 {{velocity output="false"}}
 #macro (displayObjectPropertyValue $propertyName $mode)
-  #set ($output = $doc.display($propertyName, $mode))
-  #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
-  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
-  $output
+  #unwrapXPropertyDisplay($doc.display($propertyName, $mode))
 #end
 
 #macro (displaySearchSuggestSource $source)
   #set ($icon = $source.getProperty('icon').value)
   #if ($icon.startsWith('icon:'))
diff --git a/xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/pom.xml b/xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/pom.xml
index 3ef259e7c80dc..ba95d4d3ae546 100644
--- a/xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/pom.xml
@@ -109,6 +109,27 @@
       <artifactId>xwiki-rendering-test</artifactId>
       <version>${rendering.version}</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>test-jar</id>
+            <goals>
+              <goal>test-jar</goal>
+            </goals>
+            <configuration>
+              <includes>
+                <include>**/UIExtensionScriptServiceComponentList.class</include>
+              </includes>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
 </project>
diff --git a/xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/src/test/java/org/xwiki/uiextension/script/UIExtensionScriptServiceComponentList.java b/xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/src/test/java/org/xwiki/uiextension/script/UIExtensionScriptServiceComponentList.java
new file mode 100644
index 0000000000000..1b2c55f41b385
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/src/test/java/org/xwiki/uiextension/script/UIExtensionScriptServiceComponentList.java
@@ -0,0 +1,81 @@
+/*
+ * 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.uiextension.script;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.xwiki.component.wiki.internal.DefaultWikiComponentManager;
+import org.xwiki.component.wiki.internal.DefaultWikiComponentManagerContext;
+import org.xwiki.component.wiki.internal.WikiComponentManagerEventListenerHelper;
+import org.xwiki.component.wiki.internal.bridge.DefaultContentParser;
+import org.xwiki.component.wiki.internal.bridge.DefaultWikiObjectComponentManagerEventListener;
+import org.xwiki.component.wiki.internal.bridge.WikiObjectComponentManagerEventListenerProxy;
+import org.xwiki.rendering.async.internal.block.DefaultBlockAsyncRenderer;
+import org.xwiki.test.annotation.ComponentList;
+import org.xwiki.uiextension.internal.DefaultUIExtensionManager;
+import org.xwiki.uiextension.internal.WikiUIExtensionComponentBuilder;
+import org.xwiki.uiextension.internal.filter.ExcludeFilter;
+import org.xwiki.uiextension.internal.filter.SelectFilter;
+import org.xwiki.uiextension.internal.filter.SortByCustomOrderFilter;
+import org.xwiki.uiextension.internal.filter.SortByIdFilter;
+import org.xwiki.uiextension.internal.filter.SortByParameterFilter;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Pack of default component implementations that are needed for {@link UIExtensionScriptService}.
+ *
+ * @version $Id$
+ * @since 14.10.5
+ * @since 15.1RC1
+ */
+@Documented
+@Retention(RUNTIME)
+@Target({TYPE, METHOD, ANNOTATION_TYPE})
+@ComponentList({
+    UIExtensionScriptService.class,
+    DefaultUIExtensionManager.class,
+    WikiUIExtensionComponentBuilder.class,
+    // Filters
+    ExcludeFilter.class,
+    SelectFilter.class,
+    SortByCustomOrderFilter.class,
+    SortByIdFilter.class,
+    SortByParameterFilter.class,
+    // Needed for registering the UI extensions defined in wiki pages.
+    DefaultWikiObjectComponentManagerEventListener.class,
+    WikiObjectComponentManagerEventListenerProxy.class,
+    WikiComponentManagerEventListenerHelper.class,
+    DefaultWikiComponentManager.class,
+    DefaultWikiComponentManagerContext.class,
+    // Needed for rendering the UI extensions.
+    DefaultContentParser.class,
+    DefaultBlockAsyncRenderer.class
+})
+@Inherited
+public @interface UIExtensionScriptServiceComponentList
+{
+}
diff --git a/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-directory/xwiki-platform-user-directory-ui/src/main/resources/XWiki/UserDirectoryMacros.xml b/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-directory/xwiki-platform-user-directory-ui/src/main/resources/XWiki/UserDirectoryMacros.xml
index 635df0e3eb3d3..2c90aef467b80 100644
--- a/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-directory/xwiki-platform-user-directory-ui/src/main/resources/XWiki/UserDirectoryMacros.xml
+++ b/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-directory/xwiki-platform-user-directory-ui/src/main/resources/XWiki/UserDirectoryMacros.xml
@@ -274,12 +274,11 @@
 #end
 
 #macro (displayProperty $propertyName $object $action)
   #set ($propertyClass = $object.xWikiClass.get($propertyName))
   #set ($isCheckbox = $propertyClass.getValue('displayFormType') == 'checkbox')
-  #set ($fieldDisplay = $stringtool.removeEnd($stringtool.removeStart($object.display($propertyName, $action),
-    '{{html clean="false" wiki="false"}}'), '{{/html}}'))
+  #set ($fieldDisplay = "#unwrapXPropertyDisplay($object.display($propertyName, $action))")
   &lt;dt&gt;
     &lt;label #if ($action == 'edit' &amp;&amp; !$isCheckbox)
         for="$escapetool.xml("${object.xWikiClass.name}_${object.number}_$propertyName")"#end&gt;
       #if ($isCheckbox)
         $fieldDisplay
diff --git a/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/pom.xml b/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/pom.xml
index aa1f2a7dbdfec..a54c3c45ae0d9 100644
--- a/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/pom.xml
@@ -116,7 +116,44 @@
       <groupId>org.xwiki.platform</groupId>
       <artifactId>xwiki-platform-livedata-livetable</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>
+    <!-- Provides the component list for UIExtensionScriptService. -->
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-uiextension-api</artifactId>
+      <version>${project.version}</version>
+      <classifier>tests</classifier>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <!-- Provides the component list for RenderingScriptService. -->
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-rendering-xwiki</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </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-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/src/main/resources/XWiki/XWikiUserPreferencesSheet.xml b/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/src/main/resources/XWiki/XWikiUserPreferencesSheet.xml
index 95a04be671475..ca1c1c5b73dff 100644
--- a/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/src/main/resources/XWiki/XWikiUserPreferencesSheet.xml
+++ b/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/src/main/resources/XWiki/XWikiUserPreferencesSheet.xml
@@ -18,11 +18,11 @@
  * 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.
 -->
 
-<xwikidoc version="1.3" reference="XWiki.XWikiUserPreferencesSheet" locale="">
+<xwikidoc version="1.5" reference="XWiki.XWikiUserPreferencesSheet" locale="">
   <web>XWiki</web>
   <name>XWikiUserPreferencesSheet</name>
   <language/>
   <defaultLanguage/>
   <translation>0</translation>
@@ -71,15 +71,15 @@
         <multiSelect>1</multiSelect>
         <name>async_context</name>
         <number>4</number>
         <prettyName>Context elements</prettyName>
         <relationalStorage>0</relationalStorage>
-        <separator> </separator>
+        <separator>, </separator>
         <separators>|, </separators>
         <size>5</size>
         <unmodifiable>0</unmodifiable>
-        <values>doc.reference=Document|icon.theme=Icon theme|locale=Language|rendering.defaultsyntax=Default syntax|rendering.restricted=Restricted|rendering.targetsyntax=Target syntax|request.base=Request base URL|request.parameters=Request parameters|request.url=Request URL|request.wiki=Request wiki|user=User|wiki=Wiki</values>
+        <values>action=Action|doc.reference=Document|icon.theme=Icon theme|locale=Language|rendering.defaultsyntax=Default syntax|rendering.restricted=Restricted|rendering.targetsyntax=Target syntax|request.base=Request base URL|request.cookies|request.headers|request.parameters=Request parameters|request.remoteAddr|request.url=Request URL|request.wiki=Request wiki|user=User|wiki=Wiki</values>
         <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
       </async_context>
       <async_enabled>
         <defaultValue>0</defaultValue>
         <disabled>0</disabled>
@@ -95,10 +95,11 @@
         <disabled>0</disabled>
         <editor>Text</editor>
         <name>content</name>
         <number>1</number>
         <prettyName>Executed Content</prettyName>
+        <restricted>0</restricted>
         <rows>25</rows>
         <size>120</size>
         <unmodifiable>0</unmodifiable>
         <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>
       </content>
@@ -125,10 +126,11 @@
         <disabled>0</disabled>
         <editor>PureText</editor>
         <name>parameters</name>
         <number>7</number>
         <prettyName>Extension Parameters</prettyName>
+        <restricted>0</restricted>
         <rows>10</rows>
         <size>40</size>
         <unmodifiable>0</unmodifiable>
         <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType>
       </parameters>
@@ -171,164 +173,162 @@
 #set ($discard = $doc.use($obj))
 #set ($discard = $xwiki.ssx.use('XWiki.XWikiUserSheet'))
 ###############################
 ##        CONTROLLER
 ###############################
-#if(!$obj)
-  {{info}}$services.localization.render('xe.admin.users.applyonusers'){{/info}}##
+#if (!$obj)
+  {{info}}{{translation key="xe.admin.users.applyonusers" /}}{{/info}}
 #else
   #displayFields()
 #end
 ###############################
 ##  DISPLAY FIELD OPTION LABEL
 ###############################
-#macro(displayFieldOptionLabel $option)
-  #if ($services.localization.render($option.label) != $option.label)
-    $services.localization.render($option.label)
+#macro (displayFieldOptionLabel $option)
+  #if ($services.localization.get($option.label))
+    $escapetool.xml($services.localization.render($option.label))
   #else
-    $option.value
+    $escapetool.xml($option.value)
   #end
 #end
 ###############################
 ##   DISPLAY SELECT FIELD
 ###############################
-#macro(displaySelectField $fieldName $default)
+#macro (displaySelectField $fieldName $defaultValue)
   #set ($value = $doc.getValue($fieldName, $obj))
   #set ($prop = $xclass.get($fieldName))
   #set ($options = [])
   #if ($prop.classType == 'Boolean')
     #set ($discard = $options.add({'value': 1, 'label': 'yes'}))
     #set ($discard = $options.add({'value': 0, 'label': 'no'}))
   #elseif ($prop.classType == 'Timezone')
-    #foreach($tz in $xwiki.jodatime.serverTimezone.availableIDs)
-      #set ($discard = $options.add({'value': $tz, 'label': $tz}))
-    #end
-    ## If the default value is empty (nothing has been set by the administrator), the fallback is the timezone of the 
-    ## server. But we won't display the "System Default" option, which is technical (it supposes the user knows there 
-    ## is a server with its own timezone, etc...)
-    #if ("$!default" == '')
-      #set ($default = $xwiki.jodatime.serverTimezone)
+    #foreach ($timeZoneId in $xwiki.jodatime.serverTimezone.availableIDs)
+      #set ($discard = $options.add({'value': $timeZoneId, 'label': $timeZoneId}))
     #end
   #else
-    #foreach($v in $prop.listValues)
+    #foreach ($v in $prop.listValues)
       #set ($discard = $options.add({'value': $v, 'label': "XWiki.XWikiUsers_${fieldName}_${v}"}))
     #end
   #end
+  #set ($defaultValueHint = "($escapetool.xml($services.localization.render('userprofile.default')))")
   #if ($isEdit)
-    &lt;select name="XWiki.XWikiUsers_0_${fieldName}" id="XWiki.XWikiUsers_0_${fieldName}" size="1"&gt;
-      #if ($default)
+    #set ($escapedId = $escapetool.xml("XWiki.XWikiUsers_0_${fieldName}"))
+    &lt;select name="$escapedId" id="$escapedId" size="1"&gt;
+      #if ($defaultValue)
         &lt;option value=""&gt;---&lt;/option&gt;
       #end
       #foreach ($option in $options)
         ## Some properties actually have "---" as a possible value, so we don't display it because we have
         ## already displayed it above.
         #if ($option.value != '---')
-          &lt;option value="$option.value" #if($value == $option.value)selected="selected"#end&gt;
+          &lt;option value="$escapetool.xml($option.value)"#if ($value == $option.value) selected#end&gt;
             #displayFieldOptionLabel($option)
-            #if ($default == $option.value)
-              ($services.localization.render('userprofile.default'))
+            #if ($defaultValue == $option.value)
+              $defaultValueHint
             #end
           &lt;/option&gt;
         #end
       #end
     &lt;/select&gt;
   #else
     #if ("$!value" != '')
-      $doc.display($fieldName)
+      #unwrapXPropertyDisplay($doc.display($fieldName))
     #else
       #foreach ($option in $options)
-        #if ("$!default" == "$!option.value")
+        #if ("$!defaultValue" == "$!option.value")
           #displayFieldOptionLabel($option)
-          &lt;small&gt;($services.localization.render('userprofile.default'))&lt;/small&gt;
+          &lt;small&gt;$defaultValueHint&lt;/small&gt;
         #end
       #end
     #end
   #end
 #end
 ###############################
 ##      DISPLAY FIELD
 ###############################
-#macro(displayField $fieldName $label $default)
+#macro (displayField $fieldName $label $defaultValue)
   #set ($hintKey = "XWiki.XWikiUsers_${fieldName}.hint")
   &lt;dt&gt;
-    &lt;label&gt;$services.localization.render($label)&lt;/label&gt;
+    &lt;label&gt;$!escapetool.xml($services.localization.render($label))&lt;/label&gt;
     #if ($services.localization.get($hintKey))
-      &lt;span class="xHint"&gt;$services.localization.render($hintKey)&lt;/span&gt;
+      &lt;span class="xHint"&gt;$!escapetool.xml($services.localization.render($hintKey))&lt;/span&gt;
     #end
   &lt;/dt&gt;
-  &lt;dd&gt;
+  &lt;dd data-user-property="$!escapetool.xml($fieldName)"&gt;
     #if ($fieldName == 'timezone' &amp;&amp; !$xwiki.jodatime)
-      #if ($isEdit)
-        &lt;input id="XWiki.XWikiUsers_0_${fieldName}" name="XWiki.XWikiUsers_0_${fieldName}" type="text" value="$!escapetool.xml($obj.getValue($fieldName))"/&gt;
-      #else
-        #if ("$!obj.getValue('timezone')" == '' &amp;&amp; "$!xwiki.getXWikiPreference('timezone')" != '')
-          $xwiki.getXWikiPreference('timezone') &lt;small&gt;($services.localization.render('userprofile.default'))&lt;/small&gt;
-        #else
-          $doc.display('timezone')
-        #end
-      #end
+      #unwrapXPropertyDisplay($doc.display('timezone'))
     #else
-      #displaySelectField($fieldName, $default)
+      #displaySelectField($fieldName, $defaultValue)
     #end
   &lt;/dd&gt;
 #end
 ###############################
 ##      DISPLAY FIELDS
 ###############################
-#macro(displayFields)
-{{html}}
-  &lt;div class="#if($xcontext.action == 'view')half #{else}full #{end}column xform"&gt;
+#macro (displayFields)
+{{html clean="false"}}
+  &lt;div class="#if ($xcontext.action == 'view')half #{else}full #{end}column xform"&gt;
     &lt;div class="userPreferences"&gt;
-      #if($xcontext.action == 'view' &amp;&amp; $hasEdit)
+      #if ($xcontext.action == 'view' &amp;&amp; $hasEdit)
         &lt;div class="editProfileCategory"&gt;
-          &lt;a title="$escapetool.xml($services.localization.render('platform.core.profile.category.preferences.edit'))"
+          &lt;a title="$!escapetool.xml($services.localization.render('platform.core.profile.category.preferences.edit'))"
               href="$doc.getURL('edit', 'category=preferences')" class="btn btn-xs"&gt;
             &lt;span class="action-icon"&gt;$services.icon.renderHTML('pencil')&lt;/span&gt;
-            &lt;span class='sr-only'&gt;$escapetool.xml($services.localization.render('platform.core.profile.category.preferences.edit'))&lt;/span&gt;
+            &lt;span class='sr-only'&gt;$!escapetool.xml($services.localization.render(
+              'platform.core.profile.category.preferences.edit'))&lt;/span&gt;
           &lt;/a&gt;
         &lt;/div&gt;
       #end
-      &lt;h1&gt;$services.localization.render('platform.core.profile.section.displayPreferences')&lt;/h1&gt;
+      &lt;h1&gt;$escapetool.xml($services.localization.render('platform.core.profile.section.displayPreferences'))&lt;/h1&gt;
       &lt;dl&gt;
         #displayField('displayHiddenDocuments', 'platform.core.profile.displayHiddenDocuments', '0')
         #displayField('accessibility'         , 'platform.core.profile.enableAccessibility'   , '0')
       &lt;/dl&gt;
-      &lt;h1&gt;$services.localization.render('platform.core.profile.section.localizationPreferences')&lt;/h1&gt;
+      &lt;h1&gt;$escapetool.xml($services.localization.render('platform.core.profile.section.localizationPreferences'))&lt;/h1&gt;
       &lt;dl&gt;
         #set ($defaultValue = $xwiki.getXWikiPreference('timezone'))
+        ## If the default value is empty (nothing has been set by the administrator), the fallback is the timezone of
+        ## the server. But we won't display the "System Default" option, which is technical (it supposes the user knows
+        ## there is a server with its own timezone, etc.).
+        #if ("$!defaultValue" == '')
+          #set ($defaultValue = $xwiki.jodatime.serverTimezone)
+        #end
         #displayField('timezone'              , 'platform.core.profile.timezone'              , $defaultValue)
       &lt;/dl&gt;
-      &lt;h1&gt;$services.localization.render('platform.core.profile.section.editorPreferences')&lt;/h1&gt;
+      &lt;h1&gt;$escapetool.xml($services.localization.render('platform.core.profile.section.editorPreferences'))&lt;/h1&gt;
       &lt;dl&gt;
         #set ($defaultValue = $xwiki.getXWikiPreference('editor'))
         #if ("$!defaultValue" == '')
           #set ($defaultValue = 'Text')
         #end
         #displayField('editor'                , 'platform.core.profile.editor'                , $defaultValue)
         #displayField('usertype'              , 'platform.core.profile.userType'              , 'Simple')
       &lt;/dl&gt;
-      &lt;h1&gt;$services.localization.render('platform.core.profile.section.extensionPreferences')&lt;/h1&gt;
+      &lt;h1&gt;$escapetool.xml($services.localization.render('platform.core.profile.section.extensionPreferences'))&lt;/h1&gt;
       &lt;dl&gt;
         #set ($defaultValue = $xwiki.getXWikiPreference('extensionConflictSetup'))
         #if ("$!defaultValue" == '')
           #set ($defaultValue = 0)
         #end
         #displayField('extensionConflictSetup', 'platform.core.profile.extensionConflictSetup', $defaultValue)
       &lt;/dl&gt;
     &lt;/div&gt;
   &lt;/div&gt;
-  #set($isMyProfile = ($xwiki.getDocument($xcontext.user).prefixedFullName == $doc.prefixedFullName))
-  #if(($isMyProfile || $hasAdmin) &amp;&amp; ($xcontext.action == 'view') &amp;&amp; !$doc.getObject('XWiki.LDAPProfileClass'))
+  #set ($isMyProfile = $xwiki.getDocument($xcontext.user).prefixedFullName == $doc.prefixedFullName)
+  #if (($isMyProfile || $hasAdmin) &amp;&amp; $xcontext.action == 'view' &amp;&amp; !$doc.getObject('XWiki.LDAPProfileClass'))
     &lt;div class="half column"&gt;
       &lt;div class="passwordManagement"&gt;
-        &lt;h1&gt;$services.localization.render('platform.core.profile.section.security')&lt;/h1&gt;
-        &lt;span class="buttonwrapper"&gt;&lt;a id="changePassword" href="$doc.getURL('view', 'xpage=passwd')"&gt;$services.localization.render('platform.core.profile.changePassword')&lt;/a&gt;&lt;/span&gt;
+        &lt;h1&gt;$escapetool.xml($services.localization.render('platform.core.profile.section.security'))&lt;/h1&gt;
+        &lt;span class="buttonwrapper"&gt;
+          &lt;a id="changePassword" href="$doc.getURL('view', 'xpage=passwd')"&gt;$escapetool.xml(
+            $services.localization.render('platform.core.profile.changePassword'))&lt;/a&gt;
+        &lt;/span&gt;
       &lt;/div&gt;
     &lt;/div&gt;
   #end
   &lt;div class="clearfloats"&gt;&amp;nbsp;&lt;/div&gt;
-{{/html}}##
+{{/html}}
 #end
 {{/velocity}}</content>
     </property>
     <property>
       <extensionPointId>org.xwiki.plaftorm.user.profile.menu</extensionPointId>
@@ -338,11 +338,11 @@
     </property>
     <property>
       <parameters>id=preferences
 icon=wrench
 # isActive: The user is seeing her own profile or the user is an admin.
-isActive=#if(($services.model.resolveDocument($xcontext.user) == $doc.documentReference) || $hasAdmin)true#{else}false#end
+isActive=#if ($xcontext.userReference == $doc.documentReference || $hasAdmin)true#{else}false#end
 priority=20</parameters>
     </property>
     <property>
       <scope>wiki</scope>
     </property>
diff --git a/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/src/test/java/org/xwiki/user/profile/UserProfilePageTest.java b/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/src/test/java/org/xwiki/user/profile/UserProfilePageTest.java
new file mode 100644
index 0000000000000..31a8bb687d29b
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-user/xwiki-platform-user-profile/xwiki-platform-user-profile-ui/src/test/java/org/xwiki/user/profile/UserProfilePageTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.user.profile;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.xwiki.bridge.event.DocumentCreatedEvent;
+import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.component.wiki.internal.bridge.DefaultWikiObjectComponentManagerEventListener;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.observation.EventListener;
+import org.xwiki.rendering.RenderingScriptServiceComponentList;
+import org.xwiki.rendering.internal.configuration.DefaultRenderingConfigurationComponentList;
+import org.xwiki.security.authorization.AuthorizationManager;
+import org.xwiki.test.annotation.ComponentList;
+import org.xwiki.test.page.HTML50ComponentList;
+import org.xwiki.test.page.PageTest;
+import org.xwiki.test.page.XWikiSyntax20ComponentList;
+import org.xwiki.uiextension.script.UIExtensionScriptServiceComponentList;
+
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.internal.mandatory.XWikiUsersDocumentInitializer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+/**
+ * Page test of {@code XWiki.XWikiUserSheet} that is used to display the user profile.
+ * 
+ * @version $Id$
+ * @since 14.10.5
+ * @since 15.1RC1
+ */
+@HTML50ComponentList
+@XWikiSyntax20ComponentList
+@RenderingScriptServiceComponentList
+@DefaultRenderingConfigurationComponentList
+@UIExtensionScriptServiceComponentList
+@ComponentList({XWikiUsersDocumentInitializer.class})
+class UserProfilePageTest extends PageTest
+{
+    private static final DocumentReference USER_SHEET = new DocumentReference("xwiki", "XWiki", "XWikiUserSheet");
+
+    private static final DocumentReference USER_PREFERENCES_SHEET =
+        new DocumentReference("xwiki", "XWiki", "XWikiUserPreferencesSheet");
+
+    private static final DocumentReference ADMIN_REFERENCE = new DocumentReference("xwiki", "XWiki", "Admin");
+
+    private static final DocumentReference ALICE_REFERENCE = new DocumentReference("xwiki", "XWiki", "alice");
+
+    @BeforeEach
+    void setUp() throws Exception
+    {
+        // Initialize the XWiki.XWikiUsers class (required in order to be able to display user properties).
+        this.xwiki.initializeMandatoryDocuments(this.context);
+
+        AuthorizationManager authorizationManager =
+            this.componentManager.registerMockComponent(AuthorizationManager.class);
+        when(authorizationManager.hasAccess(any(), eq(ADMIN_REFERENCE), any())).thenReturn(true);
+
+        // Register the user profile UI extensions at wiki level (requires administration rights).
+        this.componentManager.registerComponent(ComponentManager.class, "wiki", this.componentManager);
+        this.context.setUserReference(ADMIN_REFERENCE);
+        registerUIX(USER_PREFERENCES_SHEET);
+    }
+
+    @Test
+    void timeZoneDisplay() throws Exception
+    {
+        Map<String, Object> userProperties = new HashMap<>();
+        userProperties.put("timezone", "Europe/<em>Bucharest</em>");
+        createUser(userProperties);
+
+        Document dom = renderSheet(USER_SHEET);
+        Element timeZone = dom.selectFirst(".userPreferences dd[data-user-property='timezone']");
+        assertEquals(userProperties.get("timezone"), timeZone.text());
+    }
+
+    private void registerUIX(DocumentReference documentReference) throws Exception
+    {
+        XWikiDocument document = loadPage(documentReference);
+
+        // The event listeners are not registered by default. We trigger it manually so that the UIX is registered and
+        // can be found and rendered.
+        this.componentManager
+            .<EventListener>getInstance(EventListener.class,
+                DefaultWikiObjectComponentManagerEventListener.EVENT_LISTENER_NAME)
+            .onEvent(new DocumentCreatedEvent(), document, null);
+    }
+
+    private XWikiDocument createUser(Map<String, Object> userProperties) throws Exception
+    {
+        this.xwiki.createUser(ALICE_REFERENCE.getName(), userProperties, this.context);
+        XWikiDocument userDocument = this.xwiki.getDocument(ALICE_REFERENCE, this.context);
+
+        // Setup the script context as if we are logged in with the created user on its user profile.
+        this.context.setDoc(userDocument);
+        this.context.setUserReference(ALICE_REFERENCE);
+
+        return userDocument;
+    }
+
+    private Document renderSheet(DocumentReference sheetReference) throws Exception
+    {
+        XWikiDocument sheet = loadPage(sheetReference);
+
+        return Jsoup.parse(sheet.getRenderedContent(this.context));
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/displayer_timezone.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/displayer_timezone.vm
index 4793cb33f3813..e65c9d59017d8 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/displayer_timezone.vm
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/displayer_timezone.vm
@@ -15,26 +15,32 @@
 ## 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.
 ## ---------------------------------------------------------------------------
-#if ($type == 'edit' || $type == 'search')
-  #if($xwiki.jodatime)
-    <select id='$prefix$name' name='$prefix$name'>
-      <option value="" #if($value == $tz)selected="selected"#end>$services.localization.render('XWiki.XWikiPreferences_timezone_default')</option>
-      #foreach($tz in $xwiki.jodatime.getServerTimezone().getAvailableIDs())
-        <option value="$tz" #if($value == $tz)selected="selected"#end>$tz</option>
+#if ($type == 'edit' || $type == 'search' || $type == 'hidden')
+  #set ($escapedId = $escapetool.xml("${prefix}${name}"))
+  #if ($xwiki.jodatime && $type != 'hidden')
+    <select id="$!escapedId" name="$!escapedId">
+      <option value="">$!escapetool.xml($services.localization.render(
+        'XWiki.XWikiPreferences_timezone_default'))</option>
+      #foreach ($timeZoneId in $xwiki.jodatime.getServerTimezone().getAvailableIDs())
+        <option value="$!escapetool.xml($timeZoneId)"#if ($value == $timeZoneId) selected#end
+          >$!escapetool.xml($timeZoneId)</option>
       #end
     </select>
   #else
-    <input id='$prefix$name' name='$prefix$name' type="text" value="$!value"/>
+    #set ($inputType = "#if ($type == 'hidden')hidden#{else}text#end")
+    <input id="$!escapedId" name="$!escapedId" type="$!escapetool.xml($inputType)" value="$!escapetool.xml($value)" />
   #end
 #elseif ($type == 'view' || $type == 'rendered')
-$!value
-#elseif ($type == 'hidden')
-  #set ($id = $escapetool.xml("${prefix}${name}"))
-  <input type="hidden" id="$!id" name="$!id" value="$!escapetool.xml($value)" />
+## $defaultValue may be set before calling the displayer.
+#if ("$!value" == '' && "$!defaultValue" != '')
+$!escapetool.xml($defaultValue) <small>($escapetool.xml($services.localization.render('userprofile.default')))</small>
+#else
+$!escapetool.xml($value)
+#end
 #else
   ## In order for the custom displayer to be taken into account, the result of its evaluation with an unknown display
   ## mode must not be empty. Let's output something.
   Unknown display mode.
 #end
diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm
index 2055f974616be..201b7b9e97822 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/macros.vm
@@ -3015,6 +3015,20 @@ Recursive title display detected!##
 #if ($useSafeURI)
 #setVariable("$resultValue" $!{escapetool.xml($safeURI.toString())})
 #else
 #setVariable("$resultValue" '')
 #end
+#end
+
+#**
+ * The output of the api.Document#display(...) methods is wrapped in an HTML (wiki syntax) rendering macro if it contains
+ * HTML special characters or if it tries to close the outer HTML macro. This can be annoying when the output is going to
+ * be insered in another HTML macro with wiki syntax disabled, or if the output is meant to be included in a JSON and
+ * thus used on the client-side where we can't render the wiki syntax. For these use cases we need to get rid of the HTML
+ * macro wrapping.
+ *
+ * @since 14.10.5
+ * @since 15.1RC1
+ *#
+#macro (unwrapXPropertyDisplay $displayOutput)
+$!stringtool.removeEnd($stringtool.removeStart($displayOutput, '{{html clean="false" wiki="false"}}'), '{{/html}}')##
 #end
\ No newline at end of file
diff --git a/xwiki-platform-core/xwiki-platform-wiki/xwiki-platform-wiki-ui/xwiki-platform-wiki-ui-mainwiki/src/main/resources/WikiManager/WikisLiveTableResultsMacros.xml b/xwiki-platform-core/xwiki-platform-wiki/xwiki-platform-wiki-ui/xwiki-platform-wiki-ui-mainwiki/src/main/resources/WikiManager/WikisLiveTableResultsMacros.xml
index 349b4aa2e5b26..cb08654ac34d4 100644
--- a/xwiki-platform-core/xwiki-platform-wiki/xwiki-platform-wiki-ui/xwiki-platform-wiki-ui-mainwiki/src/main/resources/WikiManager/WikisLiveTableResultsMacros.xml
+++ b/xwiki-platform-core/xwiki-platform-wiki/xwiki-platform-wiki-ui/xwiki-platform-wiki-ui-mainwiki/src/main/resources/WikiManager/WikisLiveTableResultsMacros.xml
@@ -413,11 +413,11 @@
     #set($propType = '')
     #if($propClass.getPropertyNames().contains($colname))
       #set($propType = "$!propClass.get($colname).type")
     #end
     #set($fieldValue = "$!itemDoc.getValue($colname)")
-    #set($fieldDisplayValue = "$!itemDoc.display($colname, 'view')")
+    #set($fieldDisplayValue = "#unwrapXPropertyDisplay($itemDoc.display($colname, 'view'))")
     #if($fieldDisplayValue == '')
       #set($fieldDisplayValue = $services.localization.render("${request.transprefix}emptyvalue"))
     #end
     #set($fieldUrl = '')
     ## only retrieve an URL for a DBListClass item
@@ -425,11 +425,11 @@
       #set($fieldUrl = $xwiki.getURL($fieldValue))
       #if($fieldUrl == $xwiki.getURL($services.model.resolveDocument('', 'default', $doc.documentReference.extractReference('WIKI'))))
         #set($fieldUrl = '')
       #end
     #end
-    #set($discard = $row.put($colname, $fieldDisplayValue.replaceFirst($regextool.quote('{{html clean="false" wiki="false"}}'), '').replaceAll("$regextool.quote('{{/html}}')$", '')))
+    #set($discard = $row.put($colname, $fieldDisplayValue))
     #set($discard = $row.put("${colname}_value", $fieldValue))
     ######## WIKI UI MAINWIKI CUSTOMIZATION #########
     #if($colname == 'wikiprettyname')
       #set($fieldUrl = $xwiki.getURL("$wiki.mainPageReference"))
     #end

Vulnerable code:   #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
  $output
    #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
    #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
    $output
  #set ($output = $configDoc.display($propName, $action))
  #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
  $output
  #set ($output = $doc.display($configProperty.name, $displayMode))
  #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
  $!output
  #set ($output = $tdoc.display($element, $pdfTemplateObj))
  #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
  $!output
    #set($fieldDisplayValue = "$!itemDoc.display($colname, 'view')")
    #set($discard = $row.put($colname, $fieldDisplayValue.replaceFirst($regextool.quote('{{html clean="false" wiki="false"}}'), '').replaceAll("$regextool.quote('{{/html}}')$", '')))
  #set ($output = $doc.display($propName, $action))
  #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
  $output
  #set ($output = $doc.display($propertyName, $mode))
  #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}'))
  #set ($output = $stringtool.removeEnd($output, '{{/html}}'))
  $output
  #set ($fieldDisplay = $stringtool.removeEnd($stringtool.removeStart($object.display($propertyName, $action),
    '{{html clean="false" wiki="false"}}'), '{{/html}}'))
<xwikidoc version="1.3" reference="XWiki.XWikiUserPreferencesSheet" locale="">
        <separator> </separator>
        <values>doc.reference=Document|icon.theme=Icon theme|locale=Language|rendering.defaultsyntax=Default syntax|rendering.restricted=Restricted|rendering.targetsyntax=Target syntax|request.base=Request base URL|request.parameters=Request parameters|request.url=Request URL|request.wiki=Request wiki|user=User|wiki=Wiki</values>
#if(!$obj)
  {{info}}$services.localization.render('xe.admin.users.applyonusers'){{/info}}##
#macro(displayFieldOptionLabel $option)
  #if ($services.localization.render($option.label) != $option.label)
    $services.localization.render($option.label)
    $option.value
#macro(displaySelectField $fieldName $default)
    #foreach($tz in $xwiki.jodatime.serverTimezone.availableIDs)
      #set ($discard = $options.add({'value': $tz, 'label': $tz}))
    #end
    ## If the default value is empty (nothing has been set by the administrator), the fallback is the timezone of the 
    ## server. But we won't display the "System Default" option, which is technical (it supposes the user knows there 
    ## is a server with its own timezone, etc...)
    #if ("$!default" == '')
      #set ($default = $xwiki.jodatime.serverTimezone)
    #foreach($v in $prop.listValues)
    &lt;select name="XWiki.XWikiUsers_0_${fieldName}" id="XWiki.XWikiUsers_0_${fieldName}" size="1"&gt;
      #if ($default)
          &lt;option value="$option.value" #if($value == $option.value)selected="selected"#end&gt;
            #if ($default == $option.value)
              ($services.localization.render('userprofile.default'))
      $doc.display($fieldName)
        #if ("$!default" == "$!option.value")
          &lt;small&gt;($services.localization.render('userprofile.default'))&lt;/small&gt;
#macro(displayField $fieldName $label $default)
    &lt;label&gt;$services.localization.render($label)&lt;/label&gt;
      &lt;span class="xHint"&gt;$services.localization.render($hintKey)&lt;/span&gt;
  &lt;dd&gt;
      #if ($isEdit)
        &lt;input id="XWiki.XWikiUsers_0_${fieldName}" name="XWiki.XWikiUsers_0_${fieldName}" type="text" value="$!escapetool.xml($obj.getValue($fieldName))"/&gt;
      #else
        #if ("$!obj.getValue('timezone')" == '' &amp;&amp; "$!xwiki.getXWikiPreference('timezone')" != '')
          $xwiki.getXWikiPreference('timezone') &lt;small&gt;($services.localization.render('userprofile.default'))&lt;/small&gt;
        #else
          $doc.display('timezone')
        #end
      #end
      #displaySelectField($fieldName, $default)
#macro(displayFields)
{{html}}
  &lt;div class="#if($xcontext.action == 'view')half #{else}full #{end}column xform"&gt;
      #if($xcontext.action == 'view' &amp;&amp; $hasEdit)
          &lt;a title="$escapetool.xml($services.localization.render('platform.core.profile.category.preferences.edit'))"
            &lt;span class='sr-only'&gt;$escapetool.xml($services.localization.render('platform.core.profile.category.preferences.edit'))&lt;/span&gt;
      &lt;h1&gt;$services.localization.render('platform.core.profile.section.displayPreferences')&lt;/h1&gt;
      &lt;h1&gt;$services.localization.render('platform.core.profile.section.localizationPreferences')&lt;/h1&gt;
      &lt;h1&gt;$services.localization.render('platform.core.profile.section.editorPreferences')&lt;/h1&gt;
      &lt;h1&gt;$services.localization.render('platform.core.profile.section.extensionPreferences')&lt;/h1&gt;
  #set($isMyProfile = ($xwiki.getDocument($xcontext.user).prefixedFullName == $doc.prefixedFullName))
  #if(($isMyProfile || $hasAdmin) &amp;&amp; ($xcontext.action == 'view') &amp;&amp; !$doc.getObject('XWiki.LDAPProfileClass'))
        &lt;h1&gt;$services.localization.render('platform.core.profile.section.security')&lt;/h1&gt;
        &lt;span class="buttonwrapper"&gt;&lt;a id="changePassword" href="$doc.getURL('view', 'xpage=passwd')"&gt;$services.localization.render('platform.core.profile.changePassword')&lt;/a&gt;&lt;/span&gt;
{{/html}}##
isActive=#if(($services.model.resolveDocument($xcontext.user) == $doc.documentReference) || $hasAdmin)true#{else}false#end
#if ($type == 'edit' || $type == 'search')
  #if($xwiki.jodatime)
    <select id='$prefix$name' name='$prefix$name'>
      <option value="" #if($value == $tz)selected="selected"#end>$services.localization.render('XWiki.XWikiPreferences_timezone_default')</option>
      #foreach($tz in $xwiki.jodatime.getServerTimezone().getAvailableIDs())
        <option value="$tz" #if($value == $tz)selected="selected"#end>$tz</option>
    <input id='$prefix$name' name='$prefix$name' type="text" value="$!value"/>
$!value
#elseif ($type == 'hidden')
  #set ($id = $escapetool.xml("${prefix}${name}"))
  <input type="hidden" id="$!id" name="$!id" value="$!escapetool.xml($value)" />
    #set($fieldDisplayValue = "$!itemDoc.display($colname, 'view')")
    #set($discard = $row.put($colname, $fieldDisplayValue.replaceFirst($regextool.quote('{{html clean="false" wiki="false"}}'), '').replaceAll("$regextool.quote('{{/html}}')$", '')))
Fixed code:   #unwrapXPropertyDisplay($output)
    #unwrapXPropertyDisplay($output)
  #unwrapXPropertyDisplay($configDoc.display($propName, $action))
  #unwrapXPropertyDisplay($doc.display($configProperty.name, $displayMode))
  #unwrapXPropertyDisplay($tdoc.display($element, $pdfTemplateObj))
    #set($fieldDisplayValue = "#unwrapXPropertyDisplay($itemDoc.display($colname, 'view'))")
    #set($discard = $row.put($colname, $fieldDisplayValue))
  #unwrapXPropertyDisplay($doc.display($propName, $action))
  #unwrapXPropertyDisplay($doc.display($propertyName, $mode))
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <executions>
          <execution>
            <id>test-jar</id>
            <goals>
              <goal>test-jar</goal>
            </goals>
            <configuration>
              <includes>
                <include>**/UIExtensionScriptServiceComponentList.class</include>
              </includes>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
/*
 * 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.uiextension.script;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import org.xwiki.component.wiki.internal.DefaultWikiComponentManager;
import org.xwiki.component.wiki.internal.DefaultWikiComponentManagerContext;
import org.xwiki.component.wiki.internal.WikiComponentManagerEventListenerHelper;
import org.xwiki.component.wiki.internal.bridge.DefaultContentParser;
import org.xwiki.component.wiki.internal.bridge.DefaultWikiObjectComponentManagerEventListener;
import org.xwiki.component.wiki.internal.bridge.WikiObjectComponentManagerEventListenerProxy;
import org.xwiki.rendering.async.internal.block.DefaultBlockAsyncRenderer;
import org.xwiki.test.annotation.ComponentList;
import org.xwiki.uiextension.internal.DefaultUIExtensionManager;
import org.xwiki.uiextension.internal.WikiUIExtensionComponentBuilder;
import org.xwiki.uiextension.internal.filter.ExcludeFilter;
import org.xwiki.uiextension.internal.filter.SelectFilter;
import org.xwiki.uiextension.internal.filter.SortByCustomOrderFilter;
import org.xwiki.uiextension.internal.filter.SortByIdFilter;
import org.xwiki.uiextension.internal.filter.SortByParameterFilter;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Pack of default component implementations that are needed for {@link UIExtensionScriptService}.
 *
 * @version $Id$
 * @since 14.10.5
 * @since 15.1RC1
 */
@Documented
@Retention(RUNTIME)
@Target({TYPE, METHOD, ANNOTATION_TYPE})
@ComponentList({
    UIExtensionScriptService.class,
    DefaultUIExtensionManager.class,
    WikiUIExtensionComponentBuilder.class,
    // Filters
    ExcludeFilter.class,
    SelectFilter.class,
    SortByCustomOrderFilter.class,
    SortByIdFilter.class,
    SortByParameterFilter.class,
    // Needed for registering the UI extensions defined in wiki pages.
    DefaultWikiObjectComponentManagerEventListener.class,
    WikiObjectComponentManagerEventListenerProxy.class,
    WikiComponentManagerEventListenerHelper.class,
    DefaultWikiComponentManager.class,
    DefaultWikiComponentManagerContext.class,
    // Needed for rendering the UI extensions.
    DefaultContentParser.class,
    DefaultBlockAsyncRenderer.class
})
@Inherited
public @interface UIExtensionScriptServiceComponentList
{
}
  #set ($fieldDisplay = "#unwrapXPropertyDisplay($object.display($propertyName, $action))")
    <!-- 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>
    <!-- Provides the component list for UIExtensionScriptService. -->
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-uiextension-api</artifactId>
      <version>${project.version}</version>
      <classifier>tests</classifier>
      <type>test-jar</type>
      <scope>test</scope>
    </dependency>
    <!-- Provides the component list for RenderingScriptService. -->
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-rendering-xwiki</artifactId>
      <version>${project.version}</version>
      <type>test-jar</type>
      <scope>test</scope>
    </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>
<xwikidoc version="1.5" reference="XWiki.XWikiUserPreferencesSheet" locale="">
        <separator>, </separator>
        <values>action=Action|doc.reference=Document|icon.theme=Icon theme|locale=Language|rendering.defaultsyntax=Default syntax|rendering.restricted=Restricted|rendering.targetsyntax=Target syntax|request.base=Request base URL|request.cookies|request.headers|request.parameters=Request parameters|request.remoteAddr|request.url=Request URL|request.wiki=Request wiki|user=User|wiki=Wiki</values>
        <restricted>0</restricted>
        <restricted>0</restricted>
#if (!$obj)
  {{info}}{{translation key="xe.admin.users.applyonusers" /}}{{/info}}
#macro (displayFieldOptionLabel $option)
  #if ($services.localization.get($option.label))
    $escapetool.xml($services.localization.render($option.label))
    $escapetool.xml($option.value)
#macro (displaySelectField $fieldName $defaultValue)
    #foreach ($timeZoneId in $xwiki.jodatime.serverTimezone.availableIDs)
      #set ($discard = $options.add({'value': $timeZoneId, 'label': $timeZoneId}))
    #foreach ($v in $prop.listValues)
  #set ($defaultValueHint = "($escapetool.xml($services.localization.render('userprofile.default')))")
    #set ($escapedId = $escapetool.xml("XWiki.XWikiUsers_0_${fieldName}"))
    &lt;select name="$escapedId" id="$escapedId" size="1"&gt;
      #if ($defaultValue)
          &lt;option value="$escapetool.xml($option.value)"#if ($value == $option.value) selected#end&gt;
            #if ($defaultValue == $option.value)
              $defaultValueHint
      #unwrapXPropertyDisplay($doc.display($fieldName))
        #if ("$!defaultValue" == "$!option.value")
          &lt;small&gt;$defaultValueHint&lt;/small&gt;
#macro (displayField $fieldName $label $defaultValue)
    &lt;label&gt;$!escapetool.xml($services.localization.render($label))&lt;/label&gt;
      &lt;span class="xHint"&gt;$!escapetool.xml($services.localization.render($hintKey))&lt;/span&gt;
  &lt;dd data-user-property="$!escapetool.xml($fieldName)"&gt;
      #unwrapXPropertyDisplay($doc.display('timezone'))
      #displaySelectField($fieldName, $defaultValue)
#macro (displayFields)
{{html clean="false"}}
  &lt;div class="#if ($xcontext.action == 'view')half #{else}full #{end}column xform"&gt;
      #if ($xcontext.action == 'view' &amp;&amp; $hasEdit)
          &lt;a title="$!escapetool.xml($services.localization.render('platform.core.profile.category.preferences.edit'))"
            &lt;span class='sr-only'&gt;$!escapetool.xml($services.localization.render(
              'platform.core.profile.category.preferences.edit'))&lt;/span&gt;
      &lt;h1&gt;$escapetool.xml($services.localization.render('platform.core.profile.section.displayPreferences'))&lt;/h1&gt;
      &lt;h1&gt;$escapetool.xml($services.localization.render('platform.core.profile.section.localizationPreferences'))&lt;/h1&gt;
        ## If the default value is empty (nothing has been set by the administrator), the fallback is the timezone of
        ## the server. But we won't display the "System Default" option, which is technical (it supposes the user knows
        ## there is a server with its own timezone, etc.).
        #if ("$!defaultValue" == '')
          #set ($defaultValue = $xwiki.jodatime.serverTimezone)
        #end
      &lt;h1&gt;$escapetool.xml($services.localization.render('platform.core.profile.section.editorPreferences'))&lt;/h1&gt;
      &lt;h1&gt;$escapetool.xml($services.localization.render('platform.core.profile.section.extensionPreferences'))&lt;/h1&gt;
  #set ($isMyProfile = $xwiki.getDocument($xcontext.user).prefixedFullName == $doc.prefixedFullName)
  #if (($isMyProfile || $hasAdmin) &amp;&amp; $xcontext.action == 'view' &amp;&amp; !$doc.getObject('XWiki.LDAPProfileClass'))
        &lt;h1&gt;$escapetool.xml($services.localization.render('platform.core.profile.section.security'))&lt;/h1&gt;
        &lt;span class="buttonwrapper"&gt;
          &lt;a id="changePassword" href="$doc.getURL('view', 'xpage=passwd')"&gt;$escapetool.xml(
            $services.localization.render('platform.core.profile.changePassword'))&lt;/a&gt;
        &lt;/span&gt;
{{/html}}
isActive=#if ($xcontext.userReference == $doc.documentReference || $hasAdmin)true#{else}false#end
/*
 * 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.user.profile;

import java.util.HashMap;
import java.util.Map;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.xwiki.bridge.event.DocumentCreatedEvent;
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.component.wiki.internal.bridge.DefaultWikiObjectComponentManagerEventListener;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.observation.EventListener;
import org.xwiki.rendering.RenderingScriptServiceComponentList;
import org.xwiki.rendering.internal.configuration.DefaultRenderingConfigurationComponentList;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.test.annotation.ComponentList;
import org.xwiki.test.page.HTML50ComponentList;
import org.xwiki.test.page.PageTest;
import org.xwiki.test.page.XWikiSyntax20ComponentList;
import org.xwiki.uiextension.script.UIExtensionScriptServiceComponentList;

import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.internal.mandatory.XWikiUsersDocumentInitializer;

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

/**
 * Page test of {@code XWiki.XWikiUserSheet} that is used to display the user profile.
 * 
 * @version $Id$
 * @since 14.10.5
 * @since 15.1RC1
 */
@HTML50ComponentList
@XWikiSyntax20ComponentList
@RenderingScriptServiceComponentList
@DefaultRenderingConfigurationComponentList
@UIExtensionScriptServiceComponentList
@ComponentList({XWikiUsersDocumentInitializer.class})
class UserProfilePageTest extends PageTest
{
    private static final DocumentReference USER_SHEET = new DocumentReference("xwiki", "XWiki", "XWikiUserSheet");

    private static final DocumentReference USER_PREFERENCES_SHEET =
        new DocumentReference("xwiki", "XWiki", "XWikiUserPreferencesSheet");

    private static final DocumentReference ADMIN_REFERENCE = new DocumentReference("xwiki", "XWiki", "Admin");

    private static final DocumentReference ALICE_REFERENCE = new DocumentReference("xwiki", "XWiki", "alice");

    @BeforeEach
    void setUp() throws Exception
    {
        // Initialize the XWiki.XWikiUsers class (required in order to be able to display user properties).
        this.xwiki.initializeMandatoryDocuments(this.context);

        AuthorizationManager authorizationManager =
            this.componentManager.registerMockComponent(AuthorizationManager.class);
        when(authorizationManager.hasAccess(any(), eq(ADMIN_REFERENCE), any())).thenReturn(true);

        // Register the user profile UI extensions at wiki level (requires administration rights).
        this.componentManager.registerComponent(ComponentManager.class, "wiki", this.componentManager);
        this.context.setUserReference(ADMIN_REFERENCE);
        registerUIX(USER_PREFERENCES_SHEET);
    }

    @Test
    void timeZoneDisplay() throws Exception
    {
        Map<String, Object> userProperties = new HashMap<>();
        userProperties.put("timezone", "Europe/<em>Bucharest</em>");
        createUser(userProperties);

        Document dom = renderSheet(USER_SHEET);
        Element timeZone = dom.selectFirst(".userPreferences dd[data-user-property='timezone']");
        assertEquals(userProperties.get("timezone"), timeZone.text());
    }

    private void registerUIX(DocumentReference documentReference) throws Exception
    {
        XWikiDocument document = loadPage(documentReference);

        // The event listeners are not registered by default. We trigger it manually so that the UIX is registered and
        // can be found and rendered.
        this.componentManager
            .<EventListener>getInstance(EventListener.class,
                DefaultWikiObjectComponentManagerEventListener.EVENT_LISTENER_NAME)
            .onEvent(new DocumentCreatedEvent(), document, null);
    }

    private XWikiDocument createUser(Map<String, Object> userProperties) throws Exception
    {
        this.xwiki.createUser(ALICE_REFERENCE.getName(), userProperties, this.context);
        XWikiDocument userDocument = this.xwiki.getDocument(ALICE_REFERENCE, this.context);

        // Setup the script context as if we are logged in with the created user on its user profile.
        this.context.setDoc(userDocument);
        this.context.setUserReference(ALICE_REFERENCE);

        return userDocument;
    }

    private Document renderSheet(DocumentReference sheetReference) throws Exception
    {
        XWikiDocument sheet = loadPage(sheetReference);

        return Jsoup.parse(sheet.getRenderedContent(this.context));
    }
}
#if ($type == 'edit' || $type == 'search' || $type == 'hidden')
  #set ($escapedId = $escapetool.xml("${prefix}${name}"))
  #if ($xwiki.jodatime && $type != 'hidden')
    <select id="$!escapedId" name="$!escapedId">
      <option value="">$!escapetool.xml($services.localization.render(
        'XWiki.XWikiPreferences_timezone_default'))</option>
      #foreach ($timeZoneId in $xwiki.jodatime.getServerTimezone().getAvailableIDs())
        <option value="$!escapetool.xml($timeZoneId)"#if ($value == $timeZoneId) selected#end
          >$!escapetool.xml($timeZoneId)</option>
    #set ($inputType = "#if ($type == 'hidden')hidden#{else}text#end")
    <input id="$!escapedId" name="$!escapedId" type="$!escapetool.xml($inputType)" value="$!escapetool.xml($value)" />
## $defaultValue may be set before calling the displayer.
#if ("$!value" == '' && "$!defaultValue" != '')
$!escapetool.xml($defaultValue) <small>($escapetool.xml($services.localization.render('userprofile.default')))</small>
#else
$!escapetool.xml($value)
#end
#end

#**
 * The output of the api.Document#display(...) methods is wrapped in an HTML (wiki syntax) rendering macro if it contains
 * HTML special characters or if it tries to close the outer HTML macro. This can be annoying when the output is going to
 * be insered in another HTML macro with wiki syntax disabled, or if the output is meant to be included in a JSON and
 * thus used on the client-side where we can't render the wiki syntax. For these use cases we need to get rid of the HTML
 * macro wrapping.
 *
 * @since 14.10.5
 * @since 15.1RC1
 *#
#macro (unwrapXPropertyDisplay $displayOutput)
$!stringtool.removeEnd($stringtool.removeStart($displayOutput, '{{html clean="false" wiki="false"}}'), '{{/html}}')##
    #set($fieldDisplayValue = "#unwrapXPropertyDisplay($itemDoc.display($colname, 'view'))")
    #set($discard = $row.put($colname, $fieldDisplayValue))