InfoLetterSessionController.java

/*
 * Copyright (C) 2000 - 2026 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception. You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/legal/floss_exception.html"
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.silverpeas.components.infoletter.control;

import org.apache.commons.io.FileUtils;
import org.silverpeas.components.infoletter.InfoLetterException;
import org.silverpeas.components.infoletter.model.InfoLetter;
import org.silverpeas.components.infoletter.model.InfoLetterPublication;
import org.silverpeas.components.infoletter.model.InfoLetterPublicationPdC;
import org.silverpeas.components.infoletter.model.InfoLetterService;
import org.silverpeas.components.infoletter.notification.InfoLetterSubscriptionPublicationUserNotification;
import org.silverpeas.core.admin.service.AdminException;
import org.silverpeas.core.admin.service.Administration;
import org.silverpeas.core.admin.user.model.Group;
import org.silverpeas.core.admin.user.model.User;
import org.silverpeas.core.admin.user.model.UserDetail;
import org.silverpeas.core.contribution.content.ddwe.DragAndDropWbeFile;
import org.silverpeas.core.contribution.content.ddwe.model.DragAndDropWebEditorStore;
import org.silverpeas.core.contribution.content.renderer.ContributionContentRenderer;
import org.silverpeas.core.contribution.model.ContributionContent;
import org.silverpeas.core.contribution.model.ContributionIdentifier;
import org.silverpeas.core.exception.DecodingException;
import org.silverpeas.core.exception.SilverpeasException;
import org.silverpeas.core.exception.UtilTrappedException;
import org.silverpeas.core.notification.user.builder.helper.UserNotificationHelper;
import org.silverpeas.core.pdc.pdc.model.PdcClassification;
import org.silverpeas.core.pdc.pdc.model.PdcPosition;
import org.silverpeas.core.security.authorization.ComponentAccessControl;
import org.silverpeas.core.subscription.constant.SubscriberType;
import org.silverpeas.core.subscription.service.ResourceSubscriptionProvider;
import org.silverpeas.core.subscription.util.SubscriptionSubscriberMapBySubscriberType;
import org.silverpeas.core.util.Charsets;
import org.silverpeas.core.util.ServiceProvider;
import org.silverpeas.core.util.file.FileItem;
import org.silverpeas.kernel.util.Pair;
import org.silverpeas.kernel.util.StringUtil;
import org.silverpeas.core.util.URLUtil;
import org.silverpeas.core.util.csv.CSVReader;
import org.silverpeas.core.util.csv.Variant;
import org.silverpeas.core.util.file.FileRepositoryManager;
import org.silverpeas.kernel.logging.SilverLogger;
import org.silverpeas.core.web.mvc.controller.AbstractComponentSessionController;
import org.silverpeas.core.web.mvc.controller.ComponentContext;
import org.silverpeas.core.web.mvc.controller.MainSessionController;
import org.silverpeas.core.web.selection.Selection;
import org.silverpeas.core.web.selection.SelectionUsersGroups;
import org.silverpeas.core.webapi.pdc.PdcClassificationEntity;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serial;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static java.util.Collections.singleton;
import static org.silverpeas.core.pdc.pdc.model.PdcClassification.aPdcClassificationOfContent;
import static org.silverpeas.kernel.util.StringUtil.EMPTY;
import static org.silverpeas.kernel.util.StringUtil.getBooleanValue;

public class InfoLetterSessionController extends AbstractComponentSessionController {
  @Serial
  private static final long serialVersionUID = -4498344315667761189L;

  private static final String EMAILS = "Emails";

  private transient InfoLetterService service = null;

  public static final String EXPORT_CSV_NAME = "_emails.csv";

  /**
   * Standard Session Controller Constructeur
   * @param mainSessionCtrl The user's profile
   * @param componentContext The component's profile
   *
   */
  public InfoLetterSessionController(MainSessionController mainSessionCtrl,
      ComponentContext componentContext) {
    super(mainSessionCtrl, componentContext, "org.silverpeas.infoLetter.multilang.infoLetterBundle",
        "org.silverpeas.infoLetter.settings.infoLetterIcons",
        "org.silverpeas.infoLetter.settings.infoLetterSettings");
    // Initialize business interface
    if (service == null) {
      service = ServiceProvider.getService(InfoLetterService.class);
    }
  }

  /*
   * Initialize UserPanel with the list of Silverpeas subscribers
   */
  public String initUserPanel() throws InfoLetterException {
    String hostSpaceName = getSpaceLabel();
    Pair<String, String> hostComponentName = new Pair<>(getComponentLabel(),
        URLUtil.getApplicationURL() + "/RinfoLetter/" + getComponentId() + "/Main");
    String hostUrl =
        URLUtil.getApplicationURL() + "/RinfoLetter/" + getComponentId() + "/RetourPanel";
    Selection sel = getSelection();
    sel.resetAll();
    sel.setHostSpaceName(hostSpaceName);
    sel.setHostComponentName(hostComponentName);
    sel.setHostPath(null);

    sel.setGoBackURL(hostUrl);
    sel.setCancelURL(hostUrl);

    // Contraintes
    sel.setMultiSelect(true);
    sel.setPopupMode(false);

    // Internal subscribers
    SubscriptionSubscriberMapBySubscriberType subscriberIdsByTypes =
        service.getInternalSubscribers(getComponentId()).indexBySubscriberType();
    // Users
    sel.setSelectedElements(subscriberIdsByTypes.get(SubscriberType.USER).getAllIds());
    // Groups
    sel.setSelectedSets(subscriberIdsByTypes.get(SubscriberType.GROUP).getAllIds());

    return Selection.getSelectionURL();
  }

  /*
   * Retour du UserPanel
   */
  public void retourUserPanel() {
    Selection sel = getSelection();
    UserDetail[] users = SelectionUsersGroups.getUserDetails(sel.getSelectedElements());
    Group[] groups = SelectionUsersGroups.getGroups(sel.getSelectedSets());
    service.setInternalSubscribers(getComponentId(), users, groups);
  }

  // Mise à jour d'une lettre d'information
  public void updateInfoLetter(InfoLetter ie) {
    service.updateInfoLetter(ie);
  }

  // Recuperation de la liste des lettres
  public List<InfoLetter> getInfoLetters() {
    return service.getInfoLetters(getComponentId());
  }

  /**
   * Gets the newsletter template
   */
  public InfoLetter getInfoLetter() {
    return getInfoLetters().getFirst();
  }

  public DragAndDropWbeFile getTemplateFileForEdition() {
    final InfoLetter infoLetter = getInfoLetter();
    final DragAndDropWebEditorStore store =
        new DragAndDropWebEditorStore(infoLetter.getTemplateIdentifier());
    if (!store.getFile().exists() && infoLetter.existsTemplateContent()) {
      // If the Drag And Drop editor has not been yet used, taking the WYSIWYG content if any.
      // It permits retrieving old content of template edited before the introduction
      // of the Drag And Drop WEB Editor.
      final String content = infoLetter.getTemplateWysiwygContent()
          .map(ContributionContent::getRenderer)
          .map(ContributionContentRenderer::renderEdition)
          .filter(StringUtil::isDefined)
          .orElse(EMPTY);
      store.getFile().getContainer().getOrCreateTmpContent().setValue(content);
      store.getFile().getContainer().getOrCreateContent().setValue(content);
      store.save();
      infoLetter.saveTemplateContent(null);
    }
    return new DragAndDropWbeFile(store);
  }

  // Recuperation de la liste des publications
  public List<InfoLetterPublication> getInfoLetterPublications() {
    return service.getInfoLetterPublications(getInfoLetter().getId());
  }

  public DragAndDropWbeFile getFileForEditionOf(final InfoLetterPublication ilp) {
    final DragAndDropWebEditorStore store = new DragAndDropWebEditorStore(ilp.getIdentifier());
    if (!store.getFile().exists()) {
      // If the Drag And Drop editor has not been yet used, taking the WYSIWYG content if any.
      // It permits retrieving old contents of newsletter edited before the introduction
      // of the Drag And Drop WEB Editor.
      final Optional<String> legacyContent = ilp.getWysiwygContent()
          .map(ContributionContent::getRenderer)
          .map(ContributionContentRenderer::renderEdition)
          .filter(StringUtil::isDefined);
      if (legacyContent.isPresent()) {
        final String content = legacyContent.get();
        store.getFile().getContainer().getOrCreateTmpContent().setValue(content);
        store.getFile().getContainer().getOrCreateContent().setValue(content);
        store.save();
        ilp.saveContent(null);
      } else {
        ilp.initFrom(getInfoLetter());
      }
    }
    return new DragAndDropWbeFile(store);
  }

  @SuppressWarnings("UnusedReturnValue")
  public DragAndDropWbeFile resetWithTemplateFor(final InfoLetterPublication ilp) {
    final DragAndDropWebEditorStore store = new DragAndDropWebEditorStore(ilp.getIdentifier());
    ilp.initFrom(getInfoLetter());
    return new DragAndDropWbeFile(store);
  }

  // Creation d'une publication
  public void createInfoLetterPublication(InfoLetterPublicationPdC ilp) {
    ilp.setInstanceId(getComponentId());
    service.createInfoLetterPublication(ilp, getUserId());
    // Classify content on PdC
    classifyInfoLetterPublication(ilp);
  }

  /**
   * Classify the info letter publication on the PdC only if the positions attribute is filled
   * inside object parameter
   * @param ilp the InfoLetterPublication to classify
   */
  private void classifyInfoLetterPublication(InfoLetterPublicationPdC ilp) {
    String positions = ilp.getPositions();
    if (StringUtil.isDefined(positions)) {
      PdcClassificationEntity ilClassification = null;
      try {
        ilClassification = PdcClassificationEntity.fromJSON(positions);
      } catch (DecodingException e) {
        SilverLogger.getLogger(this).error("Unable to decode JSON: " + positions, e);
      }
      if (ilClassification != null && !ilClassification.isUndefined()) {
        List<PdcPosition> pdcPositions = ilClassification.getPdcPositions();
        PdcClassification classification = aPdcClassificationOfContent(ilp).
            withPositions(pdcPositions);
        // no alert of the subscribers on the PdC: they will receive the issue once it is sent
        classification.classifyContent(ilp, false);
      }
    }
  }

  // Suppression d'une publication
  public void deleteInfoLetterPublication(String id) {
    service.deleteInfoLetterPublication(ContributionIdentifier.from(getComponentId(),
        id, InfoLetterPublicationPdC.TYPE));
  }

  // Mise à jour d'une publication
  public void updateInfoLetterPublication(InfoLetterPublicationPdC ilp) {
    ilp.setInstanceId(getComponentId());
    service.updateInfoLetterPublication(ilp);
  }

  // Recuperation d'une publication par sa clef
  public InfoLetterPublicationPdC getInfoLetterPublication(String id) {
    return service.getInfoLetterPublication(ContributionIdentifier.from(getComponentId(), id,
        InfoLetterPublicationPdC.TYPE));
  }

  /**
   * Notify the newsletter to internal subscribers
   * @param ilp the info letter to send
   *
   */
  public void notifyInternalSubscribers(InfoLetterPublicationPdC ilp) {
    if (isNewsLetterSendByMail()) {
      //Send the newsletter by Mail to internal subscribers
      sendLetterByMail(ilp, getEmailsInternalRecipients(ilp));
    } else {
      //Send the newsletter via notification
      UserNotificationHelper.buildAndSend(
          new InfoLetterSubscriptionPublicationUserNotification(ilp, getUserDetail()));
    }
  }

  /**
   * Send letter by mail
   * @param ilp  the classified InfoLetterPublication to send
   * @param emails the email addresses of the receivers.
   * @return tab of dest emails in error
   */
  private String[] sendLetterByMail(InfoLetterPublicationPdC ilp, Set<String> emails) {
    Set<String> emailErrors = new LinkedHashSet<>();

    if (!emails.isEmpty()) {
      // create the Multipart and its parts to it
      String mimeMultipart = getSettings().getString("SMTPMimeMultipart", "related");

      // Subject of the mail
      String subject = getString("infoLetter.emailSubject") + ilp.getName();

      // Email address of the manager
      String emailFrom = getUserDetail().getEmailAddress();

      ilp.setInstanceId(getComponentId());
      emailErrors =
          service.sendLetterByMail(ilp, mimeMultipart, emails, subject, emailFrom);

    }
    return emailErrors.toArray(new String[0]);
  }

  /**
   * Send letter by mail to external subscribers
   * @param ilp the classified InfoLetterPublication to send
   * @return tab of emails in error
   */
  public String[] sendByMailToExternalSubscribers(InfoLetterPublicationPdC ilp) {
    // Recuperation de la liste de emails
    Set<String> extmails = getEmailsExternalsSubscribers();
    // Removing potential already sent emails
    if (isNewsLetterSendByMail()) {
      // Internal subscribers
      extmails.removeAll(getEmailsInternalRecipients(ilp));
    }
    return sendLetterByMail(ilp, extmails);
  }

  /**
   * Send letter to itself.
   */
  public String[] notifyMe(InfoLetterPublicationPdC ilp) {
    return sendLetterByMail(ilp, singleton(getUserDetail().getEmailAddress()));
  }

  /**
   * Send letter to managers
   */
  public String[] notifyManagers(InfoLetterPublicationPdC ilp) {
    // Recuperation de la liste de emails
    return sendLetterByMail(ilp, getEmailsManagers());
  }

  public Set<String> getEmailsExternalsSubscribers() {
    return service.getEmailsExternalsSubscribers(getInfoLetter().getId());
  }

  public void addExternalsSubscribers(String newMails) {
    StringTokenizer st = new StringTokenizer(newMails);
    Set<String> emails = getEmailsExternalsSubscribers();
    while (st.hasMoreTokens()) {
      String mail = st.nextToken().trim();
      if (mail.indexOf('@') > -1) { // Current address contains arobase
        emails.add(mail);
      }
    }
    service.setEmailsExternalsSubscribers(getInfoLetter().getId(), emails);
  }

  public void deleteExternalsSubscribers(String[] mails) {
    if (mails != null) {
      Set<String> curExternalEmails = getEmailsExternalsSubscribers();
      for (String email : mails) {
        curExternalEmails.remove(email);
      }
      service.setEmailsExternalsSubscribers(getInfoLetter().getId(), curExternalEmails);
    }
  }

  /**
   * Remove all external emails
   */
  public void deleteAllExternalsSubscribers() {
    Set<String> externalEmails = getEmailsExternalsSubscribers();
    externalEmails.clear();
    service.setEmailsExternalsSubscribers(getInfoLetter().getId(), externalEmails);
  }

  // Abonnement d'un utilisateur
  public void subscribeUser() {
    service.toggleSubscriber(getUserId(), getComponentId(), true);
  }

  // Désabonnement d'un utilisateur
  public void unsubscribeUser() {
    service.toggleSubscriber(getUserId(), getComponentId(), false);
  }

  // test d'abonnement d'un utilisateur interne
  public boolean isSubscriber() {
    return service.isUserSubscribed(getUserId(), getComponentId());
  }

  public boolean isPdcUsed() {
    return getBooleanValue(getComponentParameterValue("usepdc"));
  }

  /**
   * Import email addresses of external subscribers to newsletters.
   * @param filePart the uploaded CSV file.
   * @throws UtilTrappedException if an error occurs
   * @throws InfoLetterException if an error occurs during the import
   */
  public void importCsvEmails(FileItem filePart)
      throws InfoLetterException, UtilTrappedException {
    InputStream is;
    try {
      is = filePart.getInputStream();
    } catch (IOException e) {
      UtilTrappedException ie =
          new UtilTrappedException("InfoLetterSessionController.importCsvEmails",
              SilverpeasException.ERROR, "infoLetter.EX_CSV_FILE", e);
      ie.setGoBackPage(EMAILS);
      throw ie;
    }
    CSVReader csvReader = new CSVReader(getLanguage());
    csvReader.initCSVFormat("org.silverpeas.infoLetter.settings.usersCSVFormat", "User", ";");

    Variant[][] csvValues;
    try {
      csvValues = csvReader.parseStream(is);
    } catch (UtilTrappedException ute) {
      ute.setGoBackPage(EMAILS);
      throw ute;
    }

    StringBuilder listErrors = new StringBuilder();
    String email;

    for (int i = 0; i < csvValues.length; i++) {
      // email
      email = csvValues[i][0].getValueString();
      if (email.isEmpty()) {// champ obligatoire
        listErrors.append(getString("GML.ligne")).append(" = ").append(i + 1)
            .append(", ");
        listErrors.append(getString("GML.colonne")).append(" = 1, ");
        listErrors.append(getString("GML.valeur")).append(" = ").append(email).append(", ");
        listErrors.append(getString("GML.obligatoire")).append("<BR>");
      } else if (email.length() > 100) {// verifier 100 char max
        listErrors.append(getString("GML.ligne")).append(" = ").append(i + 1)
            .append(", ");
        listErrors.append(getString("GML.colonne")).append(" = 1, ");
        listErrors.append(getString("GML.valeur")).append(" = ").append(email).append(", ");
        listErrors.append(getString("GML.nbCarMax")).append(" 100 ")
            .append(getString("GML.caracteres")).append("<BR>");
      }
    }

    if (!listErrors.isEmpty()) {
      UtilTrappedException ie =
          new UtilTrappedException("InfoLetterSessionController.importCsvEmails",
              SilverpeasException.ERROR, "infoLetter.EX_CSV_FILE", listErrors.toString());
      ie.setGoBackPage(EMAILS);
      throw ie;
    }

    // no errors, email addresses are imported
    Set<String> emails = new LinkedHashSet<>();
    for (final Variant[] csvValue : csvValues) {
      // Email
      email = csvValue[0].getValueString();
      emails.add(email);
    }

    service.setEmailsExternalsSubscribers(this.getInfoLetter().getId(), emails);
  }

  /**
   * Export emails of external subscribers into a CSV file.
   * @return true if the export succeed.
   * @throws IOException if an error occurs while recording the emails to the CSV file.
   * @throws InfoLetterException if an error occurs during the export.
   */
  public boolean exportCsvEmails() throws IOException, InfoLetterException {
    File fileOutput =
        new File(FileRepositoryManager.getTemporaryPath(), getComponentId() + EXPORT_CSV_NAME);
    if (fileOutput.exists()) {//delete the existing file and recreate new one
      FileUtils.forceDelete(fileOutput);
      fileOutput =
          new File(FileRepositoryManager.getTemporaryPath(), getComponentId() + EXPORT_CSV_NAME);
    }
    try {
      Set<String> emails = getEmailsExternalsSubscribers();
      for (String email : emails) {
        FileUtils.writeStringToFile(fileOutput, email + "\n", Charsets.UTF_8, true);
      }
    } catch (Exception e) {
      throw new InfoLetterException(e);
    }
    return true;
  }

  /**
   * Get emails of component Manager
   * @return Set of emails
   */
  private Set<String> getEmailsManagers() {
    final List<String> roles = List.of("admin");
    String[] managerIds = getOrganisationController().getUsersIdsByRoleNames(getComponentId(),
        roles);
    return getEmailAddressOf(List.of(managerIds), User::isValidState);
  }

  /**
   * Get emails of internal subscribers
   * @return Set of emails
   */
  private Set<String> getEmailsInternalSubscribers(
      SubscriptionSubscriberMapBySubscriberType subscriberIdsByTypes) {
    List<String> userIds = subscriberIdsByTypes.getAllUserIds();
    return getEmailAddressOf(userIds, null);
  }

  /**
   * Gets the email addresses of all the users in Silverpeas to which the specified issue has to
   * be sent by mail: the subscribers of the newsletter and the users concerned by the issue in
   * another way, like the subscribers to a position on the PdC on which the issue is classified.
   * Unlike the subscribers of the newsletter, chosen by its managers, the latter have to be able
   * to access the application.
   * @param ilp the issue of the newsletter to send.
   * @return a set of email addresses.
   */
  private Set<String> getEmailsInternalRecipients(InfoLetterPublicationPdC ilp) {
    SubscriptionSubscriberMapBySubscriberType subscriberIdsByTypes =
        service.getInternalSubscribers(getComponentId()).indexBySubscriberType();
    Set<String> emails = getEmailsInternalSubscribers(subscriberIdsByTypes);
    ContributionIdentifier issueId =
        ContributionIdentifier.from(getComponentId(), ilp.getId(), InfoLetterPublicationPdC.TYPE);
    List<String> otherUserIds = ResourceSubscriptionProvider.getSubscribersConcernedBy(issueId)
        .getAllUserIds()
        .stream()
        .filter(u -> ComponentAccessControl.get().isUserAuthorized(u, getComponentId()))
        .toList();
    emails.addAll(getEmailAddressOf(otherUserIds, null));
    return emails;
  }

  /**
   * return true if Newsletter is sent by mail to internal users
   * @return boolean
   */
  public boolean isNewsLetterSendByMail() {
    return getBooleanValue(getComponentParameterValue("sendNewsletter"));
  }

  private Set<String> getEmailAddressOf(List<String> userIds, Predicate<UserDetail> filter) {
    // we use Administration to get user details in order to access the email addresses even for
    // the users with privacy data
    Function<String, UserDetail> userById = userId -> {
      try {
        return Administration.get().getUserDetail(userId);
      } catch (AdminException e) {
        SilverLogger.getLogger(this).error(e);
        return null;
      }
    };
    Predicate<UserDetail> filterOnUser = filter == null ? u -> true : filter;

    return userIds.stream()
        .filter(StringUtil::isDefined)
        .map(userById)
        .filter(Objects::nonNull)
        .filter(filterOnUser)
        .map(UserDetail::getEmailAddress)
        .filter(StringUtil::isDefined)
        .collect(Collectors.toSet());
  }
}