CVE-2023-50719: Cleartext Storage of Sensitive Information (CWE-312)
value: 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.

CWE description: The product stores sensitive information in cleartext within a resource that might be accessible to another control sphere.
CVSS v3 base score: 7.5
Published: 2023-12-15T19:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
Files changed: xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-api/pom.xml, xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-api/src/main/java/org/xwiki/mail/GeneralMailConfigurationUpdatedEvent.java, xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/pom.xml, xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/java/org/xwiki/mail/internal/configuration/AbstractGeneralMailConfigClassDocumentConfigurationSource.java, xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/java/org/xwiki/mail/internal/configuration/GeneralMailConfigurationUpdatedEventGenerator.java, xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/resources/META-INF/components.txt, xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/test/java/org/xwiki/mail/internal/configuration/GeneralMailConfigurationUpdatedEventGeneratorTest.java, xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrIndexer.java, xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/SolrIndexEventListener.java, xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/AbstractSolrMetadataExtractor.java, xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/DocumentSolrMetadataExtractor.java, xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/ObjectPropertySolrMetadataExtractor.java, xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/migration/R141015000XWIKI20371XWIKI21208DataMigration.java, xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/resources/META-INF/components.txt, xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/SolrIndexEventListenerTest.java, xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/metadata/DocumentSolrMetadataExtractorTest.java, xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/metadata/ObjectPropertySolrMetadataExtractorTest.java
Commit date: 2023-08-10 12:03:08 +0200
Commit message: 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.

Diff: diff --git a/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-api/pom.xml b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-api/pom.xml
index 9407f3edc0f2d..7028d5522a5bc 100644
--- a/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-api/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-api/pom.xml
@@ -42,10 +42,15 @@
     <dependency>
       <groupId>org.xwiki.commons</groupId>
       <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>
     </dependency>
   </dependencies>
diff --git a/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-api/src/main/java/org/xwiki/mail/GeneralMailConfigurationUpdatedEvent.java b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-api/src/main/java/org/xwiki/mail/GeneralMailConfigurationUpdatedEvent.java
new file mode 100644
index 0000000000000..b4ba0c324caad
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-api/src/main/java/org/xwiki/mail/GeneralMailConfigurationUpdatedEvent.java
@@ -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();
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/pom.xml b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/pom.xml
index aa94e2afc4ace..bed2f04b8bdc4 100644
--- a/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/pom.xml
@@ -71,7 +71,15 @@
     <dependency>
       <groupId>javax.servlet</groupId>
       <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>
diff --git a/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/java/org/xwiki/mail/internal/configuration/AbstractGeneralMailConfigClassDocumentConfigurationSource.java b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/java/org/xwiki/mail/internal/configuration/AbstractGeneralMailConfigClassDocumentConfigurationSource.java
index 7b72fe9f77266..efc37b28eb484 100644
--- a/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/java/org/xwiki/mail/internal/configuration/AbstractGeneralMailConfigClassDocumentConfigurationSource.java
+++ b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/java/org/xwiki/mail/internal/configuration/AbstractGeneralMailConfigClassDocumentConfigurationSource.java
@@ -41,6 +41,18 @@
     @Override
     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();
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/java/org/xwiki/mail/internal/configuration/GeneralMailConfigurationUpdatedEventGenerator.java b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/java/org/xwiki/mail/internal/configuration/GeneralMailConfigurationUpdatedEventGenerator.java
new file mode 100644
index 0000000000000..bd3d8f26ddd38
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/java/org/xwiki/mail/internal/configuration/GeneralMailConfigurationUpdatedEventGenerator.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.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();
+        }
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/resources/META-INF/components.txt
index c94734a0999ae..e959a25c1289f 100644
--- a/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/resources/META-INF/components.txt
+++ b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/main/resources/META-INF/components.txt
@@ -1,8 +1,9 @@
 org.xwiki.mail.internal.AddressesConverter
 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
diff --git a/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/test/java/org/xwiki/mail/internal/configuration/GeneralMailConfigurationUpdatedEventGeneratorTest.java b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/test/java/org/xwiki/mail/internal/configuration/GeneralMailConfigurationUpdatedEventGeneratorTest.java
new file mode 100644
index 0000000000000..39f61dd962bbd
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-general/src/test/java/org/xwiki/mail/internal/configuration/GeneralMailConfigurationUpdatedEventGeneratorTest.java
@@ -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
+                )
+            );
+        });
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrIndexer.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrIndexer.java
index 896cf6ac0ea95..00a25e818a341 100644
--- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrIndexer.java
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrIndexer.java
@@ -22,14 +22,16 @@
 import java.util.Arrays;
 import java.util.concurrent.BlockingQueue;
 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;
 import org.xwiki.component.manager.ComponentLookupException;
 import org.xwiki.component.manager.ComponentManager;
@@ -54,10 +56,11 @@
 import org.xwiki.search.solr.internal.metadata.LengthSolrInputDocument;
 import org.xwiki.search.solr.internal.metadata.SolrMetadataExtractor;
 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;
 
 /**
  * Default implementation of {@link SolrIndexer}.
  * <p>
@@ -290,10 +293,16 @@ public void runInternal()
     private ExecutionContextManager ecim;
 
     @Inject
     private JobExecutor jobs;
 
+    @Inject
+    private DocumentContextExecutor documentContextExecutor;
+
+    @Inject
+    private Provider<XWikiContext> xWikiContextProvider;
+
     /**
      * The queue of index operation to perform.
      */
     private BlockingQueue<IndexQueueEntry> indexQueue;
 
@@ -505,11 +514,24 @@ private LengthSolrInputDocument getSolrDocument(EntityReference reference)
     {
         SolrMetadataExtractor metadataExtractor = getMetadataExtractor(reference.getType());
 
         // 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;
     }
 
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/SolrIndexEventListener.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/SolrIndexEventListener.java
index b6a1885e2b3ae..b6cba1c8c1aa9 100644
--- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/SolrIndexEventListener.java
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/SolrIndexEventListener.java
@@ -33,10 +33,11 @@
 import org.xwiki.bridge.event.DocumentCreatedEvent;
 import org.xwiki.bridge.event.DocumentDeletedEvent;
 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;
 import org.xwiki.observation.event.Event;
 import org.xwiki.search.solr.internal.api.SolrIndexer;
@@ -73,11 +74,11 @@ public class SolrIndexEventListener implements EventListener
      */
     private static final List<Event> EVENTS = Arrays.<Event> asList(new DocumentUpdatedEvent(),
         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.
      */
     @Inject
@@ -173,10 +174,18 @@ public void onEvent(Event event, Object source, Object data)
             } else if (event instanceof WikiDeletedEvent) {
                 String wikiName = (String) source;
                 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);
         }
     }
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/AbstractSolrMetadataExtractor.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/AbstractSolrMetadataExtractor.java
index 2be192966da6a..e5e0dffdcd778 100644
--- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/AbstractSolrMetadataExtractor.java
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/AbstractSolrMetadataExtractor.java
@@ -37,10 +37,11 @@
 import org.apache.tika.metadata.TikaCoreProperties;
 import org.slf4j.Logger;
 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;
 import org.xwiki.model.reference.EntityReferenceSerializer;
 import org.xwiki.search.solr.internal.SolrSearchCoreUtils;
@@ -56,10 +57,11 @@
 import com.xpn.xwiki.objects.BaseObject;
 import com.xpn.xwiki.objects.BaseProperty;
 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;
 import com.xpn.xwiki.objects.classes.StaticListClass;
 import com.xpn.xwiki.objects.classes.TextAreaClass;
@@ -120,10 +122,13 @@ public abstract class AbstractSolrMetadataExtractor implements SolrMetadataExtra
     protected SolrSearchCoreUtils seachUtils;
 
     @Inject
     protected SolrLinkSerializer linkSerializer;
 
+    @Inject
+    protected GeneralMailConfiguration generalMailConfiguration;
+
     private int shortTextLimit = -1;
 
     /**
      * @return the maximum size of a short text as defined by the database
      * @since 13.2RC1
@@ -385,11 +390,11 @@ protected void setObjectContent(SolrInputDocument solrDocument, BaseObject objec
      * @param solrDocument the document to add the object property value to
      * @param property the object property whose value to add
      * @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();
         if (propertyClass instanceof StaticListClass) {
             setStaticListPropertyValue(solrDocument, property, (StaticListClass) propertyClass, locale);
@@ -429,12 +434,14 @@ private void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<Entit
             }
         } else if (propertyValue instanceof Integer && propertyClass instanceof BooleanClass) {
             // 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);
         }
     }
 
     /**
@@ -445,11 +452,11 @@ private void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<Entit
      * @param property the static list property whose value to add
      * @param propertyClass the static list class that should be used to get the list of known values
      * @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.
         Map<String, ListItem> knownValues = propertyClass.getMap(this.xcontextProvider.get());
         Object propertyValue = property.getValue();
@@ -480,11 +487,11 @@ private void setStaticListPropertyValue(SolrInputDocument solrDocument, BaseProp
      * @param property the object property instance used to get information about the property the given value
      *            corresponds to
      * @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.
         String fieldName = FieldUtils.getFieldName(FieldUtils.OBJECT_CONTENT, locale);
         String fieldValue = String.format(OBJCONTENT_FORMAT, property.getName(), typedValue.getValue());
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/DocumentSolrMetadataExtractor.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/DocumentSolrMetadataExtractor.java
index 19b84b214b61d..baee0c13a3962 100644
--- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/DocumentSolrMetadataExtractor.java
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/DocumentSolrMetadataExtractor.java
@@ -258,11 +258,11 @@ 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();
         String type = typedValue.getType();
 
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/ObjectPropertySolrMetadataExtractor.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/ObjectPropertySolrMetadataExtractor.java
index 3290feec2258f..654c67cf42699 100644
--- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/ObjectPropertySolrMetadataExtractor.java
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/metadata/ObjectPropertySolrMetadataExtractor.java
@@ -34,10 +34,11 @@
 import org.xwiki.search.solr.internal.reference.SolrReferenceResolver;
 
 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.
  * 
  * @version $Id$
@@ -87,10 +88,21 @@ public boolean setFieldsInternal(LengthSolrInputDocument solrDocument, EntityRef
         this.extractorUtils.extract(objectPropertyReference, objectProperty, solrDocument);
 
         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.
      * <p>
      * Also, index the content with each locale so that the right analyzer is used.
@@ -101,18 +113,18 @@ public boolean setFieldsInternal(LengthSolrInputDocument solrDocument, EntityRef
      * @throws Exception if problems occur.
      */
     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);
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/migration/R141015000XWIKI20371XWIKI21208DataMigration.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/migration/R141015000XWIKI20371XWIKI21208DataMigration.java
new file mode 100644
index 0000000000000..48054647590a7
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/migration/R141015000XWIKI20371XWIKI21208DataMigration.java
@@ -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;
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/resources/META-INF/components.txt
index dffe52cc2d4cd..7864b8d799fc1 100644
--- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/resources/META-INF/components.txt
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/resources/META-INF/components.txt
@@ -26,10 +26,11 @@ org.xwiki.search.solr.internal.metadata.DocumentSolrMetadataExtractor
 org.xwiki.search.solr.internal.metadata.ObjectPropertySolrMetadataExtractor
 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
 org.xwiki.search.solr.internal.reference.ObjectPropertySolrReferenceResolver
 org.xwiki.search.solr.internal.reference.ObjectSolrReferenceResolver
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/SolrIndexEventListenerTest.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/SolrIndexEventListenerTest.java
index c2459b2a03b34..57e4c64081d17 100644
--- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/SolrIndexEventListenerTest.java
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/SolrIndexEventListenerTest.java
@@ -23,12 +23,14 @@
 import java.util.Locale;
 
 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;
 import org.xwiki.test.junit5.mockito.MockComponent;
 
@@ -99,6 +101,17 @@ void onDocumentDefaultTranslationUpdated() throws Exception
         verify(this.indexer, times(3)).index(any(EntityReference.class), any(Boolean.class));
         verify(this.indexer).index(documentReference, false);
         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);
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/metadata/DocumentSolrMetadataExtractorTest.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/metadata/DocumentSolrMetadataExtractorTest.java
index f72eeea7fc590..552a81214f90c 100644
--- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/metadata/DocumentSolrMetadataExtractorTest.java
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/metadata/DocumentSolrMetadataExtractorTest.java
@@ -38,15 +38,18 @@
 import org.apache.solr.common.SolrInputDocument;
 import org.hamcrest.MatcherAssert;
 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;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.EntityReference;
@@ -73,10 +76,11 @@
 import com.xpn.xwiki.doc.XWikiDocument;
 import com.xpn.xwiki.objects.BaseObject;
 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;
 import com.xpn.xwiki.objects.classes.StringClass;
 import com.xpn.xwiki.objects.classes.TextAreaClass;
@@ -128,10 +132,13 @@
     private EntityReferenceSerializer<String> fieldNameSerializer;
 
     @MockComponent
     private SolrFieldNameEncoder fieldNameEncoder;
 
+    @MockComponent
+    private GeneralMailConfiguration mailConfiguration;
+
     @MockComponent
     @Named("document")
     private SolrReferenceResolver documentSolrReferenceResolver;
 
     private XWikiContext xcontext = mock(XWikiContext.class);
@@ -483,10 +490,76 @@ void getDocumentWithObjects() throws Exception
                 "author : " + commentAuthor, "date : " + commentDate, "list : " + commentList.get(0),
                 "list : " + commentList.get(1), "likes : " + commentLikes, "enabled : true"));
         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"
      */
     @Test
     void setStaticListPropertyValue() throws Exception
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/metadata/ObjectPropertySolrMetadataExtractorTest.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/metadata/ObjectPropertySolrMetadataExtractorTest.java
new file mode 100644
index 0000000000000..fc8f793b8e07f
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/test/java/org/xwiki/search/solr/internal/metadata/ObjectPropertySolrMetadataExtractorTest.java
@@ -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)
+        );
+    }
+
+}

Vulnerable code:             return metadataExtractor.getSolrDocument(reference);
        new XObjectPropertyUpdatedEvent(), new WikiDeletedEvent());
    private void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<EntityReference> property,
        } else if (!(propertyClass instanceof PasswordClass)) {
            // Avoid indexing passwords.
    private void setStaticListPropertyValue(SolrInputDocument solrDocument, BaseProperty<EntityReference> property,
    protected void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<EntityReference> property,
    protected void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<EntityReference> property,
            solrDocument.setField(FieldUtils.getFieldName(FieldUtils.PROPERTY_VALUE, documentLocale),
                objectProperty.getValue());
        solrDocument.setField(FieldUtils.getFieldName(FieldUtils.PROPERTY_VALUE, null), objectProperty.getValue());
Fixed code:     <dependency>
      <groupId>org.xwiki.commons</groupId>
      <artifactId>xwiki-commons-observation-api</artifactId>
      <version>${commons.version}</version>
    </dependency>
/*
 * 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();
    }
}
    <!-- 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>

    /**
     * Clear the cache.
     *
     * @since 14.10.15
     * @since 15.5.2
     * @since 15.7RC1
     */
    void clearCache()
    {
        this.cache.removeAll();
    }
/*
 * 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();
        }
    }
}
org.xwiki.mail.internal.configuration.GeneralMailConfigurationUpdatedEventGenerator
/*
 * 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
                )
            );
        });
    }
}
import javax.inject.Provider;
import org.xwiki.bridge.internal.DocumentContextExecutor;
import com.xpn.xwiki.doc.XWikiDocument;
    @Inject
    private DocumentContextExecutor documentContextExecutor;

    @Inject
    private Provider<XWikiContext> xWikiContextProvider;

            // 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);
            }
import org.xwiki.mail.GeneralMailConfigurationUpdatedEvent;
        new XObjectPropertyUpdatedEvent(), new WikiDeletedEvent(), new GeneralMailConfigurationUpdatedEvent());
            } 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);
                }
import org.xwiki.mail.GeneralMailConfiguration;
import com.xpn.xwiki.objects.classes.EmailClass;
    @Inject
    protected GeneralMailConfiguration generalMailConfiguration;

    protected void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<?> property,
        } else if (!(propertyClass instanceof PasswordClass)
            && !((propertyClass instanceof EmailClass) && this.generalMailConfiguration.shouldObfuscate()))
        {
            // Avoid indexing passwords and, when obfuscation is enabled, emails.
    private void setStaticListPropertyValue(SolrInputDocument solrDocument, BaseProperty<?> property,
    protected void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<?> property,
    protected void setPropertyValue(SolrInputDocument solrDocument, BaseProperty<?> property,
import com.xpn.xwiki.objects.classes.PropertyClass;
    @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());
    }

        PropertyClass propertyClass = objectProperty.getPropertyClass(this.xcontextProvider.get());
            setPropertyValue(solrDocument, objectProperty, propertyClass, documentLocale);
        setPropertyValue(solrDocument, objectProperty, propertyClass, null);
/*
 * 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;
    }
}
org.xwiki.search.solr.internal.migration.R141015000XWIKI20371XWIKI21208DataMigration
import org.xwiki.mail.GeneralMailConfigurationUpdatedEvent;
import org.xwiki.model.reference.WikiReference;

    @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);
    }
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.xwiki.mail.GeneralMailConfiguration;
import com.xpn.xwiki.objects.classes.EmailClass;
    @MockComponent
    private GeneralMailConfiguration mailConfiguration;

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

}