CVE-2023-50719 (Java, published 2023-12-15T19:15Z)
Description: 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.

Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-312 \u2014 Cleartext Storage of Sensitive Information
The product stores sensitive information in cleartext within a resource that might be accessible to another control sphere.
Repository: https://github.com/xwiki/xwiki-platform
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 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.


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

}