CVE-2023-50719 (published 2023-12-15T19:15Z)

XWiki Platform is a generic wiki platform. Starting in 7.2-milestone-2 and prior to versions 14.10.15, 15.5.2, and 15.7-rc-1, the Solr-based search in XWiki discloses the password hashes of all users to anyone with view right on the respective user profiles. By default, all user profiles are public. This vulnerability also affects any configurations used by extensions that contain passwords like API keys that are viewable for the attacker. Normally, such passwords aren't accessible but this vulnerability would disclose them as plain text.  This has been patched in XWiki 14.10.15, 15.5.2 and 15.7RC1. There are no known workarounds for this vulnerability.\n

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

Weakness information:
cwe_id: CWE-312
cwe_name: Cleartext Storage of Sensitive Information
description: The product stores sensitive information in cleartext within a resource that might be accessible to another control sphere.
extended_description: 
	 Because the information is stored in cleartext (i.e., unencrypted), attackers could potentially read it. Even if the information is encoded in a way that is not human-readable, certain techniques could determine which encoding is being used, then decode the information.
	 When organizations adopt cloud services, it can be easier for attackers to access the data from anywhere on the Internet.
	 In some systems/environments such as cloud, the use of "double encryption" (at both the software and hardware layer) might be required, and the developer might be solely responsible for both layers, instead of shared responsibility with the administrator of the broader system/environment.
       
         

Fix information:
repo_url: https://github.com/xwiki/xwiki-platform
commit_details: repo_url: https://github.com/xwiki/xwiki-platform
author: Michael Hamann
author_date: 2023-08-08 17:02:58+02:00
msg: XWIKI-20371: Consider mail obfuscation settings in the Solr indexer

* Introduce a new event GeneralMailConfigurationUpdatedEvent to notify
  the indexer when the mail configuration changed.
* Don't index emails when obfuscation is enabled.
* Add a test for the object property metadata extractor.
* Add a migration to clear the index.
* Make sure that indexing is executed with the indexed document in
  context.
repository: repo_url: https://github.com/xwiki/xwiki-platform
repo_name: xwiki/xwiki-platform
description: The XWiki platform
repo_language: Java
owner: xwiki
file_changes: filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -44,6 +44,11 @@
       <artifactId>xwiki-commons-component-api</artifactId>
       <version>${commons.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.xwiki.commons</groupId>
+      <artifactId>xwiki-commons-observation-api</artifactId>
+      <version>${commons.version}</version>
+    </dependency>
     <dependency>
       <groupId>com.sun.mail</groupId>
       <artifactId>jakarta.mail</artifactId>

programming_language: HTML
filename: GeneralMailConfigurationUpdatedEvent.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,108 @@
+/*
+ * 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.mail;
+
+import java.util.Objects;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.xwiki.observation.event.Event;
+import org.xwiki.stability.Unstable;
+
+/**
+ * An event triggered after the general mail configuration has been changed.
+ * <p>
+ * The event also sends the following parameters:
+ * </p>
+ * <ul>
+ * <li>source: the wiki id (as string) where the configuration has been changed, or {@code null} if the change was on
+ * the main wiki and thus affects all wikis.</li>
+ * <li>data: {@code null}</li>
+ * </ul>
+ * <p>This event is intentionally not serializable as it will be triggered on every node of the cluster separately.</p>
+ *
+ * @since 14.10.15
+ * @since 15.5.2
+ * @since 15.7RC1
+ * @version $Id$
+ */
+@Unstable
+public class GeneralMailConfigurationUpdatedEvent implements Event
+{
+    private String wikiId;
+
+    /**
+     * Default constructor, used to get notified about a mail configuration change in any wiki or to trigger an event
+     * for a configuration change on the main wiki.
+     */
+    public GeneralMailConfigurationUpdatedEvent()
+    {
+    }
+
+    /**
+     * An event for changes that affect the passed wiki id. Used to get notified about a change that affects the
+     * specified wiki or to trigger an event on a subwiki.
+     *
+     * @param wikiId the id of the wiki where the configuration has been changed
+     */
+    public GeneralMailConfigurationUpdatedEvent(String wikiId)
+    {
+        this.wikiId = wikiId;
+    }
+
+    @Override
+    public boolean matches(Object otherEvent)
+    {
+        if (otherEvent == this) {
+            return true;
+        }
+
+        boolean isMatching = false;
+
+        if (this.getClass().isAssignableFrom(otherEvent.getClass())) {
+            GeneralMailConfigurationUpdatedEvent other = (GeneralMailConfigurationUpdatedEvent) otherEvent;
+            isMatching = this.wikiId == null || other.wikiId == null || Objects.equals(this.wikiId, other.wikiId);
+        }
+
+        return isMatching;
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        GeneralMailConfigurationUpdatedEvent that = (GeneralMailConfigurationUpdatedEvent) o;
+
+        return new EqualsBuilder().append(this.wikiId, that.wikiId).isEquals();
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return new HashCodeBuilder(17, 37).append(this.wikiId).toHashCode();
+    }
+}

programming_language: Java
filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -73,5 +73,13 @@
       <artifactId>javax.servlet-api</artifactId>
       <scope>test</scope>
     </dependency>
+    <!-- Needed for ReferenceComponentList -->
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-oldcore</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 </project>

programming_language: HTML
filename: AbstractGeneralMailConfigClassDocumentConfigurationSource.java
change_type: ModificationType.MODIFY
diff: @@ -43,4 +43,16 @@ protected LocalDocumentReference getClassReference()
     {
         return GENERAL_MAILCONFIGCLASS_REFERENCE;
     }
+
+    /**
+     * Clear the cache.
+     *
+     * @since 14.10.15
+     * @since 15.5.2
+     * @since 15.7RC1
+     */
+    void clearCache()
+    {
+        this.cache.removeAll();
+    }
 }

programming_language: Java
filename: GeneralMailConfigurationUpdatedEventGenerator.java
change_type: ModificationType.ADD
diff: @@ -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.mail.internal.configuration;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+import org.xwiki.configuration.ConfigurationSource;
+import org.xwiki.mail.GeneralMailConfigurationUpdatedEvent;
+import org.xwiki.model.reference.EntityReferenceSerializer;
+import org.xwiki.model.reference.LocalDocumentReference;
+import org.xwiki.model.reference.RegexEntityReference;
+import org.xwiki.observation.EventListener;
+import org.xwiki.observation.ObservationManager;
+import org.xwiki.observation.event.Event;
+
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.internal.event.XObjectAddedEvent;
+import com.xpn.xwiki.internal.event.XObjectDeletedEvent;
+import com.xpn.xwiki.internal.event.XObjectUpdatedEvent;
+import com.xpn.xwiki.objects.BaseObjectReference;
+
+/**
+ * Event generator for {@link GeneralMailConfigurationUpdatedEvent}.
+ *
+ * @since 14.10.15
+ * @since 15.5.2
+ * @since 15.7RC1
+ * @version $Id$
+ */
+@Component
+@Named(GeneralMailConfigurationUpdatedEventGenerator.NAME)
+@Singleton
+public class GeneralMailConfigurationUpdatedEventGenerator implements EventListener
+{
+    static final String NAME = "generalmailconfigurationchangedeventgenerator";
+
+    @Inject
+    protected EntityReferenceSerializer<String> referenceSerializer;
+
+    @Inject
+    private Provider<ObservationManager> observationManagerProvider;
+
+    @Inject
+    private Provider<XWikiContext> contextProvider;
+
+    @Inject
+    @Named("mailgeneral")
+    private ConfigurationSource currentWikiMailConfigSource;
+
+    @Inject
+    @Named("mailgeneralmainwiki")
+    private ConfigurationSource mainWikiMailConfigSource;
+
+    @Override
+    public String getName()
+    {
+        return NAME;
+    }
+
+    @Override
+    public List<Event> getEvents()
+    {
+        String serializedClassReference = this.referenceSerializer.serialize(
+            AbstractGeneralMailConfigClassDocumentConfigurationSource.GENERAL_MAILCONFIGCLASS_REFERENCE);
+        RegexEntityReference filter = BaseObjectReference.any(serializedClassReference);
+
+        return List.of(new XObjectAddedEvent(filter), new XObjectDeletedEvent(filter), new XObjectUpdatedEvent(filter));
+    }
+
+    @Override
+    public void onEvent(Event event, Object source, Object data)
+    {
+        ObservationManager observationManager = this.observationManagerProvider.get();
+
+        if (source instanceof XWikiDocument) {
+            XWikiDocument document = (XWikiDocument) source;
+
+            // Test that the document is really a mail configuration document.
+            LocalDocumentReference localDocumentReference = new LocalDocumentReference(document.getDocumentReference());
+            if (!AbstractMailConfigClassDocumentConfigurationSource.MAILCONFIG_REFERENCE
+                .equals(localDocumentReference))
+            {
+                return;
+            }
+
+            // Clear the cache of the current wiki mail config source to ensure that any listener of the new events will
+            // see the new configuration value regardless of which listener is called first.
+            clearCache(this.currentWikiMailConfigSource);
+
+            // Get the wiki id from the document reference. If it is the main wiki, notify without wiki, otherwise
+            // notify with the wiki id.
+            String wikiId = document.getDocumentReference().getWikiReference().getName();
+            if (this.contextProvider.get().isMainWiki(wikiId)) {
+                // Clear the cache of the main wiki mail config source to ensure that any listener of the events will
+                // see the new configuration value regardless of which listener is called first.
+                clearCache(this.mainWikiMailConfigSource);
+                observationManager.notify(new GeneralMailConfigurationUpdatedEvent(), null);
+            } else {
+                observationManager.notify(new GeneralMailConfigurationUpdatedEvent(wikiId), wikiId);
+            }
+        }
+    }
+
+    private void clearCache(ConfigurationSource mainWikiMailConfigSource)
+    {
+        if (mainWikiMailConfigSource instanceof AbstractGeneralMailConfigClassDocumentConfigurationSource) {
+            ((AbstractGeneralMailConfigClassDocumentConfigurationSource) mainWikiMailConfigSource).clearCache();
+        }
+    }
+}

programming_language: Java
filename: components.txt
change_type: ModificationType.MODIFY
diff: @@ -3,6 +3,7 @@ org.xwiki.mail.internal.AddressConverter
 org.xwiki.mail.internal.InternetAddressConverter
 org.xwiki.mail.internal.configuration.DefaultGeneralMailConfiguration
 org.xwiki.mail.internal.configuration.GeneralMailConfigClassDocumentConfigurationSource
+org.xwiki.mail.internal.configuration.GeneralMailConfigurationUpdatedEventGenerator
 org.xwiki.mail.internal.configuration.MainWikiGeneralMailConfigClassDocumentConfigurationSource
 org.xwiki.mail.internal.DefaultEmailAddressObfuscator
 org.xwiki.mail.script.GeneralMailScriptService

programming_language: Scala
filename: GeneralMailConfigurationUpdatedEventGeneratorTest.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,233 @@
+/*
+ * 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.mail.internal.configuration;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.xwiki.cache.Cache;
+import org.xwiki.cache.CacheException;
+import org.xwiki.cache.CacheManager;
+import org.xwiki.cache.config.CacheConfiguration;
+import org.xwiki.mail.GeneralMailConfigurationUpdatedEvent;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.reference.ObjectReference;
+import org.xwiki.model.reference.WikiReference;
+import org.xwiki.observation.EventListener;
+import org.xwiki.observation.ObservationManager;
+import org.xwiki.observation.event.Event;
+import org.xwiki.observation.internal.DefaultObservationManager;
+import org.xwiki.test.annotation.BeforeComponent;
+import org.xwiki.test.annotation.ComponentList;
+import org.xwiki.test.junit5.mockito.ComponentTest;
+import org.xwiki.test.junit5.mockito.InjectMockComponents;
+import org.xwiki.test.junit5.mockito.MockComponent;
+import org.xwiki.test.mockito.MockitoComponentManager;
+
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.internal.event.XObjectAddedEvent;
+import com.xpn.xwiki.internal.event.XObjectDeletedEvent;
+import com.xpn.xwiki.internal.event.XObjectUpdatedEvent;
+import com.xpn.xwiki.objects.BaseObjectReference;
+import com.xpn.xwiki.test.reference.ReferenceComponentList;
+import com.xpn.xwiki.web.Utils;
+
+import static org.junit.jupiter.api.Named.named;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link GeneralMailConfigurationUpdatedEventGenerator}.
+ *
+ * @version $Id$
+ */
+@ComponentTest
+@ComponentList({
+    DefaultObservationManager.class})
+@ReferenceComponentList
+class GeneralMailConfigurationUpdatedEventGeneratorTest
+{
+    private static final String MAIN_WIKI = "main";
+
+    private static final String CURRENT_WIKI_CACHE_ID = "configuration.document.mail.general";
+
+    private static final String MAIN_WIKI_CACHE_ID = "configuration.document.mail.general.mainwiki";
+
+    @MockComponent
+    private CacheManager cacheManager;
+
+    /**
+     * The mail configuration for the current wiki, needed by the eventGenerator, injected here to make
+     * sure its members are all mocks. A real instance is used here as otherwise the cache invalidation cannot be
+     * tested.
+     */
+    @InjectMockComponents
+    private GeneralMailConfigClassDocumentConfigurationSource currentWikiConfigurationSource;
+
+    @InjectMockComponents
+    private MainWikiGeneralMailConfigClassDocumentConfigurationSource mainWikiConfigurationSource;
+
+    @InjectMockComponents
+    private GeneralMailConfigurationUpdatedEventGenerator eventGenerator;
+
+    @Inject
+    private ObservationManager observationManager;
+
+    @MockComponent
+    private XWikiContext context;
+
+    private Cache<Object> mainWikiCache;
+
+    private Cache<Object> currentWikiCache;
+
+    @BeforeComponent
+    private void mockCache(MockitoComponentManager componentManager) throws CacheException
+    {
+        // Can't use @Mock annotation as mocks are injected too late.
+        this.mainWikiCache = mock();
+        this.currentWikiCache = mock();
+        when(this.cacheManager.createNewCache(any())).then(invocation -> {
+            CacheConfiguration configuration = invocation.getArgument(0);
+            if (configuration.getConfigurationId().equals(CURRENT_WIKI_CACHE_ID)) {
+                return this.currentWikiCache;
+            } else if (MAIN_WIKI_CACHE_ID.equals(configuration.getConfigurationId())) {
+                return this.mainWikiCache;
+            }
+
+            throw new IllegalArgumentException("Unknown cache configuration ID ["
+                + configuration.getConfigurationId() + "]");
+        });
+
+        when(this.context.isMainWiki(MAIN_WIKI)).thenReturn(true);
+        Utils.setComponentManager(componentManager);
+    }
+
+    @ParameterizedTest
+    @MethodSource("eventParametersSource")
+    void onEvent(String wiki, Callable<Event> eventCallable, Object source, String newEventSource) throws Exception
+    {
+        Event event = eventCallable.call();
+
+        // Unregister the listeners of the configuration sources themselves to check that cache invalidation is
+        // really triggered independently.
+        this.observationManager.removeListener(CURRENT_WIKI_CACHE_ID);
+        this.observationManager.removeListener(MAIN_WIKI_CACHE_ID);
+        // Register the listener explicitly as it seems that it isn't registered automatically.
+        this.observationManager.addListener(this.eventGenerator);
+
+        EventListener allWikiListener = mock();
+        when(allWikiListener.getEvents()).thenReturn(List.of(new GeneralMailConfigurationUpdatedEvent()));
+        when(allWikiListener.getName()).thenReturn("allWiki");
+
+        EventListener currentWikiListener = mock();
+        when(currentWikiListener.getEvents()).thenReturn(List.of(new GeneralMailConfigurationUpdatedEvent(wiki)));
+        when(currentWikiListener.getName()).thenReturn("currentWiki");
+
+        EventListener wikiListener = mock();
+        when(wikiListener.getEvents()).thenReturn(List.of(new GeneralMailConfigurationUpdatedEvent("wiki")));
+        when(wikiListener.getName()).thenReturn("wiki");
+
+        this.observationManager.addListener(allWikiListener);
+        this.observationManager.addListener(currentWikiListener);
+        this.observationManager.addListener(wikiListener);
+
+        this.observationManager.notify(event, source);
+
+        verify(this.currentWikiCache).removeAll();
+        if (MAIN_WIKI.equals(wiki)) {
+            verify(this.mainWikiCache).removeAll();
+        } else {
+            verify(this.mainWikiCache, never()).removeAll();
+        }
+
+        if (List.of(MAIN_WIKI, "wiki").contains(wiki)) {
+            verify(wikiListener).onEvent(any(), eq(newEventSource), eq(null));
+        } else {
+            verify(wikiListener, never()).onEvent(any(), any(), any());
+        }
+
+        verify(allWikiListener).onEvent(any(), eq(newEventSource), eq(null));
+        verify(currentWikiListener).onEvent(any(), eq(newEventSource), eq(null));
+    }
+
+    private static ObjectReference getConfigurationObjectReference(String wikiId)
+    {
+        DocumentReference documentReference = new DocumentReference(
+            AbstractGeneralMailConfigClassDocumentConfigurationSource.MAILCONFIG_REFERENCE, new WikiReference(wikiId));
+        DocumentReference configClassReference = new DocumentReference(
+            AbstractGeneralMailConfigClassDocumentConfigurationSource.GENERAL_MAILCONFIGCLASS_REFERENCE,
+            new WikiReference(wikiId));
+        return new BaseObjectReference(configClassReference, 0, documentReference);
+    }
+
+    static Stream<Arguments> eventParametersSource()
+    {
+        // We cannot call getConfigurationObjectReference() here as the component manager hasn't been set on Utils
+        // yet and thus initializing the BaseObjectReference fails. Therefore, this returns callables that do the call.
+        return Stream.of("wiki", "otherwiki", MAIN_WIKI).flatMap(wiki -> {
+            DocumentReference documentReference =
+                new DocumentReference(
+                    AbstractGeneralMailConfigClassDocumentConfigurationSource.MAILCONFIG_REFERENCE,
+                    new WikiReference(wiki)
+                );
+            Object source = new XWikiDocument(documentReference);
+            String newEventSource = MAIN_WIKI.equals(wiki) ? null : wiki;
+
+            return Stream.of(
+                arguments(
+                    wiki,
+                    named("XObjectUpdatedEvent",
+                        (Callable<Event>) () -> new XObjectUpdatedEvent(getConfigurationObjectReference(wiki))
+                    ),
+                    source,
+                    newEventSource
+                ),
+                arguments(
+                    wiki,
+                    named("XObjectDeletedEvent",
+                        (Callable<Event>) () -> new XObjectDeletedEvent(getConfigurationObjectReference(wiki))
+                    ),
+                    source,
+                    newEventSource
+                ),
+                arguments(
+                    wiki,
+                    named("XObjectAddedEvent",
+                        (Callable<Event>) () -> new XObjectAddedEvent(getConfigurationObjectReference(wiki))
+                    ),
+                    source,
+                    newEventSource
+                )
+            );
+        });
+    }
+}

programming_language: Java
filename: DefaultSolrIndexer.java
change_type: ModificationType.MODIFY
diff: @@ -24,10 +24,12 @@
 import java.util.concurrent.LinkedBlockingQueue;
 
 import javax.inject.Inject;
+import javax.inject.Provider;
 import javax.inject.Singleton;
 
 import org.apache.solr.common.SolrInputDocument;
 import org.slf4j.Logger;
+import org.xwiki.bridge.internal.DocumentContextExecutor;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.component.annotation.DisposePriority;
 import org.xwiki.component.manager.ComponentLifecycleException;
@@ -56,6 +58,7 @@
 import org.xwiki.search.solr.internal.reference.SolrReferenceResolver;
 
 import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.doc.XWikiDocument;
 import com.xpn.xwiki.util.AbstractXWikiRunnable;
 
 /**
@@ -292,6 +295,12 @@ public void runInternal()
     @Inject
     private JobExecutor jobs;
 
+    @Inject
+    private DocumentContextExecutor documentContextExecutor;
+
+    @Inject
+    private Provider<XWikiContext> xWikiContextProvider;
+
     /**
      * The queue of index operation to perform.
      */
@@ -507,7 +516,20 @@ private LengthSolrInputDocument getSolrDocument(EntityReference reference)
 
         // If the entity type is supported, use the extractor to get the SolrInputDocuent.
         if (metadataExtractor != null) {
-            return metadataExtractor.getSolrDocument(reference);
+            // Set the document that belongs to the entity reference as context document to ensure that the correct
+            // settings are loaded for the current document/wiki.
+            XWikiContext context = this.xWikiContextProvider.get();
+            try {
+                XWikiDocument document = context.getWiki().getDocument(reference, context);
+
+                return this.documentContextExecutor.call(() -> metadataExtractor.getSolrDocument(reference), document);
+            } catch (SolrIndexerException | IllegalArgumentException e) {
+                // Re-throw to avoid wrapping exceptions that are declared in the method signature.
+                throw e;
+            } catch (Exception e) {
+                throw new SolrIndexerException("Error executing the indexer in the context of the document to index",
+                    e);
+            }
         }
 
         return null;

programming_language: Java
filename: SolrIndexEventListener.java
change_type: ModificationType.MODIFY
diff: @@ -35,6 +35,7 @@
 import org.xwiki.bridge.event.DocumentUpdatedEvent;
 import org.xwiki.bridge.event.WikiDeletedEvent;
 import org.xwiki.component.annotation.Component;
+import org.xwiki.mail.GeneralMailConfigurationUpdatedEvent;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.WikiReference;
 import org.xwiki.observation.EventListener;
@@ -75,7 +76,7 @@ public class SolrIndexEventListener implements EventListener
         new DocumentCreatedEvent(), new DocumentDeletedEvent(), new AttachmentAddedEvent(),
         new AttachmentDeletedEvent(), new AttachmentUpdatedEvent(), new XObjectAddedEvent(), new XObjectDeletedEvent(),
         new XObjectUpdatedEvent(), new XObjectPropertyAddedEvent(), new XObjectPropertyDeletedEvent(),
-        new XObjectPropertyUpdatedEvent(), new WikiDeletedEvent());
+        new XObjectPropertyUpdatedEvent(), new WikiDeletedEvent(), new GeneralMailConfigurationUpdatedEvent());
 
     /**
      * Logging framework.
@@ -175,6 +176,14 @@ public void onEvent(Event event, Object source, Object data)
                 WikiReference wikiReference = new WikiReference(wikiName);
 
                 this.solrIndexer.get().delete(wikiReference, false);
+            } else if (event instanceof GeneralMailConfigurationUpdatedEvent) {
+                // Refresh the index when the mail configuration is changed because the mail configuration is used to
+                // decide if emails shall be indexed or not.
+                if (source instanceof String) {
+                    this.solrIndexer.get().index(new WikiReference((String) source), true);
+                } else {
+                    this.solrIndexer.get().index(null, true);
+                }
             }
         } catch (Exception e) {
             this.logger.error("Failed to handle event [{}] with source [{}]", event, source.toString(), e);

programming_language: Java
filename: AbstractSolrMetadataExtractor.java
change_type: ModificationType.MODIFY
diff: @@ -39,6 +39,7 @@
 import org.xwiki.component.manager.ComponentLookupException;
 import org.xwiki.component.manager.ComponentManager;
 import org.xwiki.context.Execution;
+import org.xwiki.mail.GeneralMailConfiguration;
 import org.xwiki.model.EntityType;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.EntityReference;
@@ -58,6 +59,7 @@
 import com.xpn.xwiki.objects.StringProperty;
 import com.xpn.xwiki.objects.classes.BaseClass;
 import com.xpn.xwiki.objects.classes.BooleanClass;
+import com.xpn.xwiki.objects.classes.EmailClass;
 import com.xpn.xwiki.objects.classes.ListItem;
 import com.xpn.xwiki.objects.classes.PasswordClass;
 import com.xpn.xwiki.objects.classes.PropertyClass;
@@ -122,6 +124,9 @@ public abstract class AbstractSolrMetadataExtractor implements SolrMetadataExtra
     @Inject
     protected SolrLinkSerializer linkSerializer;
 
+    @Inject
+    protected GeneralMailConfiguration generalMailConfiguration;
+
     private int shortTextLimit = -1;
 
     /**
@@ -387,7 +392,7 @@ protected void setObjectContent(SolrInputDocument solrDocument, BaseObject objec
      * @param propertyClass the class that describes the given property
      * @param locale the locale of the indexed document
      */
-    private void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<EntityReference> property,
+    protected void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<?> property,
         PropertyClass propertyClass, Locale locale)
     {
         Object propertyValue = property.getValue();
@@ -431,8 +436,10 @@ private void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<Entit
             // Boolean properties are stored as integers (0 is false and 1 is true).
             Boolean booleanValue = ((Integer) propertyValue) != 0;
             setPropertyValue(solrDocument, property, new TypedValue(booleanValue), locale);
-        } else if (!(propertyClass instanceof PasswordClass)) {
-            // Avoid indexing passwords.
+        } else if (!(propertyClass instanceof PasswordClass)
+            && !((propertyClass instanceof EmailClass) && this.generalMailConfiguration.shouldObfuscate()))
+        {
+            // Avoid indexing passwords and, when obfuscation is enabled, emails.
             setPropertyValue(solrDocument, property, new TypedValue(propertyValue), locale);
         }
     }
@@ -447,7 +454,7 @@ private void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<Entit
      * @param locale the locale of the indexed document
      * @see "XWIKI-9417: Search does not return any results for Static List values"
      */
-    private void setStaticListPropertyValue(SolrInputDocument solrDocument, BaseProperty<EntityReference> property,
+    private void setStaticListPropertyValue(SolrInputDocument solrDocument, BaseProperty<?> property,
         StaticListClass propertyClass, Locale locale)
     {
         // The list of known values specified in the XClass.
@@ -482,7 +489,7 @@ private void setStaticListPropertyValue(SolrInputDocument solrDocument, BaseProp
      * @param typedValue the value to add
      * @param locale the locale of the indexed document
      */
-    protected void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<EntityReference> property,
+    protected void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<?> property,
         TypedValue typedValue, Locale locale)
     {
         // Collect all the property values from all the objects of a document in a single (localized) field.

programming_language: Java
filename: DocumentSolrMetadataExtractor.java
change_type: ModificationType.MODIFY
diff: @@ -260,7 +260,7 @@ protected void setObjects(SolrInputDocument solrDocument, Locale locale, XWikiDo
     }
 
     @Override
-    protected void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<EntityReference> property,
+    protected void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<?> property,
         TypedValue typedValue, Locale locale)
     {
         Object value = typedValue.getValue();

programming_language: Scala
filename: ObjectPropertySolrMetadataExtractor.java
change_type: ModificationType.MODIFY
diff: @@ -36,6 +36,7 @@
 import com.xpn.xwiki.doc.XWikiDocument;
 import com.xpn.xwiki.objects.BaseObjectReference;
 import com.xpn.xwiki.objects.BaseProperty;
+import com.xpn.xwiki.objects.classes.PropertyClass;
 
 /**
  * Extract the metadata to be indexed from object properties.
@@ -89,6 +90,17 @@ public boolean setFieldsInternal(LengthSolrInputDocument solrDocument, EntityRef
         return true;
     }
 
+    @Override
+    protected void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<?> property,
+        TypedValue typedValue, Locale locale)
+    {
+        String fieldName = FieldUtils.getFieldName(FieldUtils.PROPERTY_VALUE, locale);
+        // The current method can be called multiple times for the same property value (but with a different type).
+        // Since we don't care about the value type here (all the values are collected in a localized field) we need to
+        // make sure we don't add the same value twice. Derived classes can override this method and use the value type.
+        addFieldValueOnce(solrDocument, fieldName, typedValue.getValue());
+    }
+
     /**
      * Set the locale to all the translations that the owning document has. This ensures that this entity is found for
      * all the translations of a document, not just the original document.
@@ -103,16 +115,16 @@ public boolean setFieldsInternal(LengthSolrInputDocument solrDocument, EntityRef
     protected void setLocaleAndContentFields(DocumentReference documentReference, SolrInputDocument solrDocument,
         BaseProperty<ObjectPropertyReference> objectProperty) throws Exception
     {
+        PropertyClass propertyClass = objectProperty.getPropertyClass(this.xcontextProvider.get());
         // Do the work for each locale.
         for (Locale documentLocale : getLocales(documentReference, null)) {
             solrDocument.addField(FieldUtils.LOCALES, documentLocale);
 
-            solrDocument.setField(FieldUtils.getFieldName(FieldUtils.PROPERTY_VALUE, documentLocale),
-                objectProperty.getValue());
+            setPropertyValue(solrDocument, objectProperty, propertyClass, documentLocale);
         }
 
         // We can`t rely on the schema's copyField here because we would trigger it for each locale. Doing the copy to
         // the text_general field manually.
-        solrDocument.setField(FieldUtils.getFieldName(FieldUtils.PROPERTY_VALUE, null), objectProperty.getValue());
+        setPropertyValue(solrDocument, objectProperty, propertyClass, null);
     }
 }

programming_language: Scala
filename: R141015000XWIKI20371XWIKI21208DataMigration.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,128 @@
+/*
+ * 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.search.solr.internal.migration;
+
+import java.io.IOException;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.solr.client.solrj.SolrServerException;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.context.Execution;
+import org.xwiki.context.ExecutionContext;
+import org.xwiki.search.solr.internal.api.SolrInstance;
+
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.store.migration.DataMigrationException;
+import com.xpn.xwiki.store.migration.XWikiDBVersion;
+import com.xpn.xwiki.store.migration.hibernate.HibernateDataMigration;
+
+/**
+ * Migration in charge of emptying the Search solr core in order to perform a reindex of all documents after an
+ * indexing bug has been fixed.
+ *
+ * @version $Id$
+ * @since 14.10.15
+ * @since 15.5.1
+ * @since 15.7RC1
+ */
+@Component
+@Named(R141015000XWIKI20371XWIKI21208DataMigration.HINT)
+@Singleton
+// Note that we implement HibernateDataMigration and not DataMigration only because of XWIKI-19399
+public class R141015000XWIKI20371XWIKI21208DataMigration implements HibernateDataMigration
+{
+    /**
+     * Hint of the migration.
+     */
+    public static final String HINT = "R141015000XWIKI20371XWIKI21208";
+
+    @Inject
+    private SolrInstance solrInstance;
+
+    @Inject
+    private Execution execution;
+
+    /**
+     * @return XWikiContext to access the store
+     */
+    private XWikiContext getXWikiContext()
+    {
+        ExecutionContext context = this.execution.getContext();
+        return (XWikiContext) context.getProperty("xwikicontext");
+    }
+
+    @Override
+    public String getName()
+    {
+        return HINT;
+    }
+
+    @Override
+    public String getDescription()
+    {
+        return "Clear the index to purge sensitive data from the index.";
+    }
+
+    @Override
+    public XWikiDBVersion getVersion()
+    {
+        // Change to 141015000 for 14.10.15.
+        // Use 15.05.02 for 15.5.2 and 15.7RC1 as 15.05.01 is the most recent migration.
+        return new XWikiDBVersion(150502000);
+    }
+
+    @Override
+    public void migrate() throws DataMigrationException
+    {
+        try {
+            this.solrInstance.deleteByQuery("*:*");
+            this.solrInstance.commit();
+        } catch (SolrServerException | IOException e) {
+            throw new DataMigrationException("Error while performing Solr query to empty the search core", e);
+        }
+    }
+
+    @Override
+    public boolean shouldExecute(XWikiDBVersion startupVersion)
+    {
+        XWikiDBVersion ltsVersion = new XWikiDBVersion(141015000);
+        XWikiDBVersion afterLTSVersion = new XWikiDBVersion(150000000);
+        // Execute the migration if the version is either before the LTS version or equal to or larger than the
+        // afterLTSVersion and before the version of this migration.
+        // We only need to execute this migration once on the main wiki.
+        return getXWikiContext().isMainWiki() && (startupVersion.compareTo(ltsVersion) < 0
+            || (startupVersion.compareTo(afterLTSVersion) >= 0 && startupVersion.compareTo(getVersion()) < 0));
+    }
+
+    @Override
+    public String getPreHibernateLiquibaseChangeLog() throws DataMigrationException
+    {
+        return null;
+    }
+
+    @Override
+    public String getLiquibaseChangeLog() throws DataMigrationException
+    {
+        return null;
+    }
+}

programming_language: Java
filename: components.txt
change_type: ModificationType.MODIFY
diff: @@ -28,6 +28,7 @@ org.xwiki.search.solr.internal.metadata.ObjectSolrMetadataExtractor
 org.xwiki.search.solr.internal.metadata.SolrLinkSerializer
 org.xwiki.search.solr.internal.metadata.SolrMetadataExtractorUtils
 org.xwiki.search.solr.internal.migration.R141005000XWIKI20575XWIKI20619DataMigration
+org.xwiki.search.solr.internal.migration.R141015000XWIKI20371XWIKI21208DataMigration
 org.xwiki.search.solr.internal.reference.AttachmentSolrReferenceResolver
 org.xwiki.search.solr.internal.reference.DefaultSolrReferenceResolver
 org.xwiki.search.solr.internal.reference.DocumentSolrReferenceResolver

programming_language: Scala
filename: SolrIndexEventListenerTest.java
change_type: ModificationType.MODIFY
diff: @@ -25,8 +25,10 @@
 import org.junit.jupiter.api.Test;
 import org.xwiki.bridge.event.DocumentDeletedEvent;
 import org.xwiki.bridge.event.DocumentUpdatedEvent;
+import org.xwiki.mail.GeneralMailConfigurationUpdatedEvent;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.EntityReference;
+import org.xwiki.model.reference.WikiReference;
 import org.xwiki.search.solr.internal.api.SolrIndexer;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
@@ -101,4 +103,15 @@ void onDocumentDefaultTranslationUpdated() throws Exception
         verify(this.indexer).index(new DocumentReference(documentReference, Locale.FRENCH), false);
         verify(this.indexer).index(new DocumentReference(documentReference, Locale.GERMAN), false);
     }
+
+    @Test
+    void onGeneralMailConfigurationUpdatedEvent()
+    {
+        this.listener.onEvent(new GeneralMailConfigurationUpdatedEvent(), null, null);
+        verify(this.indexer).index(null, true);
+
+        String otherWiki = "otherwiki";
+        this.listener.onEvent(new GeneralMailConfigurationUpdatedEvent(otherWiki), otherWiki, null);
+        verify(this.indexer).index(new WikiReference(otherWiki), true);
+    }
 }

programming_language: Java
filename: DocumentSolrMetadataExtractorTest.java
change_type: ModificationType.MODIFY
diff: @@ -40,11 +40,14 @@
 import org.hamcrest.Matchers;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.AdditionalAnswers;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 import org.xwiki.context.Execution;
 import org.xwiki.context.ExecutionContext;
+import org.xwiki.mail.GeneralMailConfiguration;
 import org.xwiki.model.EntityType;
 import org.xwiki.model.document.DocumentAuthors;
 import org.xwiki.model.reference.AttachmentReference;
@@ -75,6 +78,7 @@
 import com.xpn.xwiki.objects.BaseProperty;
 import com.xpn.xwiki.objects.classes.BaseClass;
 import com.xpn.xwiki.objects.classes.BooleanClass;
+import com.xpn.xwiki.objects.classes.EmailClass;
 import com.xpn.xwiki.objects.classes.ListItem;
 import com.xpn.xwiki.objects.classes.PasswordClass;
 import com.xpn.xwiki.objects.classes.StaticListClass;
@@ -130,6 +134,9 @@
     @MockComponent
     private SolrFieldNameEncoder fieldNameEncoder;
 
+    @MockComponent
+    private GeneralMailConfiguration mailConfiguration;
+
     @MockComponent
     @Named("document")
     private SolrReferenceResolver documentSolrReferenceResolver;
@@ -485,6 +492,72 @@ void getDocumentWithObjects() throws Exception
         assertEquals(8, objectProperties.size());
     }
 
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void getDocumentWithEmailObject(boolean obfuscate) throws Exception
+    {
+        when(this.mailConfiguration.shouldObfuscate()).thenReturn(obfuscate);
+
+        // Mock the user object
+        BaseObject userObject = mock(BaseObject.class);
+        // Mock the email property
+        String email = "test@example.com";
+        BaseProperty<EntityReference> emailField = mock(BaseProperty.class);
+        when(emailField.getName()).thenReturn("email");
+        when(emailField.getValue()).thenReturn(email);
+        when(emailField.getObject()).thenReturn(userObject);
+
+        // Mock the class reference
+        DocumentReference userClassRef = new DocumentReference("wiki", "space", "userClass");
+
+        // Add the mocked email object
+        when(this.document.getXObjects())
+            .thenReturn(Collections.singletonMap(userClassRef, Arrays.asList(userObject)));
+
+        // Mock the class
+        BaseClass xclass = mock(BaseClass.class);
+        when(userObject.getXClass(this.xcontext)).thenReturn(xclass);
+        when(userObject.getFieldList()).thenReturn(List.of(emailField));
+        when(userObject.getRelativeXClassReference())
+            .thenReturn(userClassRef.removeParent(userClassRef.getWikiReference()));
+
+        when(xclass.get("email")).thenReturn(mock(EmailClass.class));
+
+        //
+        // Call
+        //
+        SolrInputDocument solrDocument = this.metadataExtractor.getSolrDocument(this.frenchDocumentReference);
+
+        //
+        // Assert and verify
+        //
+        String serializedUserClass = "space.userClass";
+        assertEquals(List.of(serializedUserClass), solrDocument.getFieldValues(FieldUtils.CLASS));
+
+        // Make sure the password is not indexed (neither as a string nor as a localized text).
+        String emailProperty = "property." + serializedUserClass + ".email";
+        String emailString = emailProperty + "_string";
+        String emailSortString = emailProperty + "_sortString";
+        if (obfuscate) {
+            assertNull(solrDocument.getFieldValue(emailString));
+            assertNull(solrDocument.getFieldValue(emailSortString));
+
+            assertNull(solrDocument
+                .getFieldValue(FieldUtils.getFieldName(emailProperty, Locale.FRENCH)));
+        } else {
+            assertEquals(email, solrDocument.getFieldValue(emailString));
+            assertEquals(email, solrDocument.getFieldValue(emailSortString));
+        }
+
+        Collection<Object> objectProperties =
+            solrDocument.getFieldValues(FieldUtils.getFieldName("object." + serializedUserClass, Locale.FRENCH));
+        assertEquals(obfuscate ? null : List.of(email), objectProperties);
+
+        objectProperties =
+            solrDocument.getFieldValues(FieldUtils.getFieldName(FieldUtils.OBJECT_CONTENT, Locale.FRENCH));
+        assertEquals(obfuscate ? null : List.of("email : " + email), objectProperties);
+    }
+
     /**
      * @see "XWIKI-9417: Search does not return any results for Static List values"
      */

programming_language: Java
filename: ObjectPropertySolrMetadataExtractorTest.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,218 @@
+/*
+ * 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.search.solr.internal.metadata;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Stream;
+
+import javax.inject.Provider;
+
+import org.apache.solr.common.SolrInputDocument;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.xwiki.context.Execution;
+import org.xwiki.context.ExecutionContext;
+import org.xwiki.mail.GeneralMailConfiguration;
+import org.xwiki.model.document.DocumentAuthors;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.reference.ObjectPropertyReference;
+import org.xwiki.search.solr.internal.SolrSearchCoreUtils;
+import org.xwiki.search.solr.internal.api.FieldUtils;
+import org.xwiki.test.annotation.ComponentList;
+import org.xwiki.test.junit5.mockito.ComponentTest;
+import org.xwiki.test.junit5.mockito.InjectMockComponents;
+import org.xwiki.test.junit5.mockito.MockComponent;
+import org.xwiki.test.mockito.MockitoComponentManager;
+
+import com.xpn.xwiki.XWiki;
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.BaseObject;
+import com.xpn.xwiki.objects.BaseObjectReference;
+import com.xpn.xwiki.objects.BaseProperty;
+import com.xpn.xwiki.objects.classes.BaseClass;
+import com.xpn.xwiki.objects.classes.EmailClass;
+import com.xpn.xwiki.objects.classes.PasswordClass;
+import com.xpn.xwiki.objects.classes.PropertyClass;
+import com.xpn.xwiki.objects.classes.StaticListClass;
+import com.xpn.xwiki.objects.classes.StringClass;
+import com.xpn.xwiki.test.reference.ReferenceComponentList;
+import com.xpn.xwiki.web.Utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit test for {@link ObjectPropertySolrMetadataExtractor}.
+ *
+ * @version $Id$
+ */
+@ComponentTest
+@ComponentList({ SolrSearchCoreUtils.class, SolrLinkSerializer.class })
+@ReferenceComponentList
+class ObjectPropertySolrMetadataExtractorTest
+{
+    @InjectMockComponents
+    private ObjectPropertySolrMetadataExtractor metadataExtractor;
+
+    @MockComponent
+    private Provider<XWikiContext> contextProvider;
+
+    @MockComponent
+    private Execution execution;
+
+    @MockComponent
+    private GeneralMailConfiguration mailConfiguration;
+
+    private final XWikiContext xcontext = mock(XWikiContext.class);
+
+    /**
+     * The document from which we extract the meta data.
+     */
+    private final XWikiDocument document = mock(XWikiDocument.class);
+
+    private final XWikiDocument translatedDocument = mock(XWikiDocument.class, Locale.FRENCH.toString());
+
+    private final DocumentReference documentReference =
+        new DocumentReference("wiki", Arrays.asList("Path", "To", "Page"), "WebHome");
+
+    private final DocumentReference frenchDocumentReference =
+        new DocumentReference(this.documentReference, Locale.FRENCH);
+
+    private final DocumentAuthors documentAuthors = mock(DocumentAuthors.class);
+
+    @BeforeEach
+    void setUp(MockitoComponentManager componentManager) throws Exception
+    {
+        // XWikiContext Provider
+        when(this.contextProvider.get()).thenReturn(this.xcontext);
+
+        // XWikiContext trough Execution
+        ExecutionContext executionContext = new ExecutionContext();
+        executionContext.setProperty(XWikiContext.EXECUTIONCONTEXT_KEY, this.xcontext);
+        when(this.execution.getContext()).thenReturn(executionContext);
+
+        // XWiki
+        XWiki wiki = mock(XWiki.class);
+        when(this.xcontext.getWiki()).thenReturn(wiki);
+
+        // XWikiDocument
+        when(wiki.getDocument(this.documentReference, this.xcontext)).thenReturn(this.document);
+        when(wiki.getDocument(this.frenchDocumentReference, this.xcontext)).thenReturn(this.translatedDocument);
+        when(this.document.getDocumentReference()).thenReturn(this.documentReference);
+        when(this.document.isHidden()).thenReturn(false);
+        when(this.document.getLocale()).thenReturn(Locale.ROOT);
+        when(this.document.getRealLocale()).thenReturn(Locale.US);
+        when(this.document.getAuthors()).thenReturn(this.documentAuthors);
+
+        when(this.document.getTranslatedDocument(Locale.FRENCH, this.xcontext)).thenReturn(this.translatedDocument);
+        when(this.translatedDocument.getRealLocale()).thenReturn(Locale.FRENCH);
+        when(this.translatedDocument.getLocale()).thenReturn(Locale.FRENCH);
+        when(this.translatedDocument.getDocumentReference()).thenReturn(this.frenchDocumentReference);
+        when(this.translatedDocument.getAuthors()).thenReturn(this.documentAuthors);
+
+        // Make sure that Utils.getComponent works (used in BaseObjectReference).
+        Utils.setComponentManager(componentManager);
+    }
+
+    @ParameterizedTest
+    @MethodSource("getDocumentWithPropertyParameters")
+    void getDocumentWithProperty(PropertyClass propertyClass, Object value, boolean obfuscate, boolean visible)
+        throws Exception
+    {
+        when(this.mailConfiguration.shouldObfuscate()).thenReturn(obfuscate);
+        // Setup object
+        BaseObject object = mock(BaseObject.class);
+
+        // Setup property
+        BaseProperty<ObjectPropertyReference> property = mock(BaseProperty.class);
+        String propertyName = "property";
+        when(property.getName()).thenReturn(propertyName);
+        when(property.getValue()).thenReturn(value);
+        when(property.getObject()).thenReturn(object);
+
+        // Mock the class reference
+        DocumentReference classReference = new DocumentReference("wiki", Arrays.asList("Path", "To"), "Class");
+
+        BaseClass xclass = mock(BaseClass.class);
+        when(object.getXClass(this.xcontext)).thenReturn(xclass);
+        when(object.getFieldList()).thenReturn(List.of(property));
+        when(property.getPropertyClass(this.xcontext)).thenReturn(propertyClass);
+        when(object.getXClassReference()).thenReturn(classReference);
+        when(object.getRelativeXClassReference())
+            .thenReturn(classReference.removeParent(classReference.getWikiReference()));
+        when(xclass.get(propertyName)).thenReturn(propertyClass);
+
+        BaseObjectReference objectReference = new BaseObjectReference(classReference, 0, this.documentReference);
+        ObjectPropertyReference propertyReference = new ObjectPropertyReference(propertyName, objectReference);
+        when(property.getReference()).thenReturn(propertyReference);
+        when(this.document.getXObjectProperty(propertyReference)).thenReturn(property);
+
+        // Construct a property reference based on the French document for getting the document.
+        BaseObjectReference frenchObjectReference =
+            new BaseObjectReference(classReference, 0, this.frenchDocumentReference);
+        ObjectPropertyReference frenchPropertyReference =
+            new ObjectPropertyReference(propertyName, frenchObjectReference);
+        when(this.document.getXObjectProperty(frenchPropertyReference)).thenReturn(property);
+
+        SolrInputDocument solrDocument = this.metadataExtractor.getSolrDocument(frenchPropertyReference);
+
+        // Verify the fields
+        String serializedClassReference = "Path.To.Class";
+        assertEquals(List.of(serializedClassReference), solrDocument.getFieldValues(FieldUtils.CLASS));
+        assertEquals(0, solrDocument.getFieldValue(FieldUtils.NUMBER));
+        assertEquals(propertyName, solrDocument.getFieldValue(FieldUtils.PROPERTY_NAME));
+        String fieldName = FieldUtils.getFieldName(FieldUtils.PROPERTY_VALUE, null);
+        String localizedFieldName = FieldUtils.getFieldName(FieldUtils.PROPERTY_VALUE, Locale.FRENCH);
+        if (visible) {
+            List<?> values;
+            if (value instanceof List) {
+                values = (List<?>) value;
+            } else {
+                values = Collections.singletonList(value);
+            }
+            assertEquals(values, solrDocument.getFieldValues(fieldName));
+            assertEquals(values, solrDocument.getFieldValues(localizedFieldName));
+        } else {
+            assertNull(solrDocument.getFieldValue(fieldName));
+            assertNull(solrDocument.getFieldValue(localizedFieldName));
+        }
+    }
+
+    static Stream<Arguments> getDocumentWithPropertyParameters()
+    {
+        return Stream.of(
+            arguments(mock(StringClass.class), "value", false, true),
+            arguments(mock(EmailClass.class), "email@example.com", false, true),
+            arguments(mock(EmailClass.class), "hidden@example.com", true, false),
+            arguments(mock(PasswordClass.class), "passw0rd", false, false),
+            arguments(mock(StaticListClass.class), List.of("red", "green"), false, true)
+        );
+    }
+
+}

programming_language: Scala