CVE-2023-40176 (XML, published 2023-08-23T20:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
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.
Repository: https://github.com/xwiki/xwiki-platform
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 message: XWIKI-20276: Improve escaping in the timezone displayer


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