001/*
002 *  Copyright 2011 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.translationflagging;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Objects;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.activity.Initializable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.commons.lang3.LocaleUtils;
033import org.apache.commons.lang3.StringUtils;
034import org.apache.commons.lang3.tuple.Pair;
035
036import org.ametys.cms.repository.WorkflowAwareContent;
037import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
038import org.ametys.core.right.RightManager;
039import org.ametys.core.ui.mail.StandardMailBodyHelper;
040import org.ametys.core.user.UserIdentity;
041import org.ametys.core.user.UserManager;
042import org.ametys.core.util.I18nUtils;
043import org.ametys.core.util.language.UserLanguagesManager;
044import org.ametys.core.util.mail.SendMailHelper;
045import org.ametys.plugins.repository.AmetysObjectResolver;
046import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
047import org.ametys.plugins.workflow.EnhancedFunction;
048import org.ametys.runtime.config.Config;
049import org.ametys.runtime.i18n.I18nizableText;
050import org.ametys.runtime.plugin.component.PluginAware;
051import org.ametys.web.repository.content.WebContent;
052import org.ametys.web.repository.page.Page;
053import org.ametys.web.repository.site.Site;
054
055import com.opensymphony.module.propertyset.PropertySet;
056import com.opensymphony.workflow.WorkflowException;
057
058import jakarta.mail.MessagingException;
059
060/**
061 * When a content is saved, this workflow function looks if the pages it belongs to are translated in other languages.
062 * If this is the case, an alert e-mail is sent to all the persons who are responsible for modifying the translated pages,
063 * to inform them that a new version is available.
064 */
065public class TranslationAlertFunction extends AbstractContentWorkflowComponent implements EnhancedFunction, Initializable, PluginAware
066{
067    
068    /** The e-mail subject i18n key. */
069    public static final String I18N_KEY_SUBJECT = "PLUGINS_TRANSLATIONFLAGGING_ALERT_EMAIL_SUBJECT";
070    
071    /** The e-mail body's title i18n key. */
072    public static final String I18N_KEY_BODY_TITLE = "PLUGINS_TRANSLATIONFLAGGING_ALERT_EMAIL_BODY_TITLE";
073    
074    /** The e-mail body i18n key. */
075    public static final String I18N_KEY_BODY = "PLUGINS_TRANSLATIONFLAGGING_ALERT_EMAIL_BODY";
076    
077    /** The users manager. */
078    protected UserManager _userManager;
079    
080    /** The rights manager. */
081    protected RightManager _rightManager;
082    
083    /** The i18n utils. */
084    protected I18nUtils _i18nUtils;
085    
086    /** The ametys object resolver. */
087    protected AmetysObjectResolver _resolver;
088    
089    /** The user languages manager. */
090    protected UserLanguagesManager _userLanguagesManager;
091    
092    /** The plugin name. */
093    protected String _pluginName;
094    
095    /** The server base URL. */
096    protected String _baseUrl;
097    
098    @Override
099    public void setPluginInfo(String pluginName, String featureName, String id)
100    {
101        _pluginName = pluginName;
102    }
103    
104    @Override
105    public void initialize() throws Exception
106    {
107        _baseUrl = StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html");
108        if (!_baseUrl.endsWith("/"))
109        {
110            _baseUrl = _baseUrl + "/";
111        }
112    }
113    
114    @Override
115    public void service(ServiceManager serviceManager) throws ServiceException
116    {
117        super.service(serviceManager);
118        _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE);
119        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
120        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
121        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
122        _userLanguagesManager = (UserLanguagesManager) serviceManager.lookup(UserLanguagesManager.ROLE);
123    }
124    
125    @Override
126    public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException
127    {
128        _logger.info("Performing translation alerts workflow function.");
129        
130        // Retrieve current content.
131        WorkflowAwareContent content = getContent(transientVars);
132        
133        if (content instanceof WebContent && !_contentHelper.isMultilingual(content))
134        {
135            WebContent webContent = (WebContent) content;
136            Site site = webContent.getSite();
137            
138            // The content has to be a web content to be referenced by pages.
139            boolean enabled = site.getValue("translationflagging-enable-alerts", false, false);
140            if (enabled)
141            {
142                sendAlerts((WebContent) content);
143            }
144        }
145    }
146    
147    /**
148     * Send the alerts to tell users the translated content was modified.
149     * @param content the modified content.
150     */
151    protected void sendAlerts(WebContent content)
152    {
153        // Process all the pages which reference the content.
154        for (Page page : content.getReferencingPages())
155        {
156            Site site = content.getSite();
157            // Get the master language for this site.
158            String masterLanguage = site.getValue("master-language");
159            
160            // Process the page only if it's in the master language, or there is no master language.
161            if (StringUtils.isEmpty(masterLanguage) || page.getSitemapName().equals(masterLanguage))
162            {
163                // Get the translated versions of the page.
164                Collection<Page> translatedPages = getTranslations(page).values();
165                
166                for (Page translatedPage : translatedPages)
167                {
168                    // Get the users to sent the alert to.
169                    HashSet<UserIdentity> users = getUsersToNotify(translatedPage);
170                    
171                    // Build and send the alert.
172                    sendAlert(page, content, translatedPage, users);
173                }
174            }
175        }
176    }
177    
178    /**
179     * Build and send an alert e-mail to inform of a translation to a list of users.
180     * @param page the modified page.
181     * @param content the content which was modified.
182     * @param translatedPage the translated page.
183     * @param users the users to send the e-mail to.
184     */
185    protected void sendAlert(Page page, WebContent content, Page translatedPage, Set<UserIdentity> users)
186    {
187        Site site = page.getSite();
188        String mailFrom = site.getValue("site-mail-from");
189        
190        String defaultLanguage = _userLanguagesManager.getDefaultLanguage();
191        
192        Map<String, List<String>> recipientsByLanguage = users.stream()
193            .map(_userManager::getUser)
194            .filter(Objects::nonNull)
195            .map(user -> Pair.of(user.getLanguage(), user.getEmail()))
196            .filter(p -> StringUtils.isNotEmpty(p.getRight()))
197            .collect(Collectors.groupingBy(
198                    p -> {
199                        return StringUtils.defaultIfBlank(p.getLeft(), defaultLanguage);
200                    },
201                    Collectors.mapping(
202                            Pair::getRight,
203                            Collectors.toList()
204                    )
205                )
206            );
207        
208        String siteTitle = page.getSite().getTitle();
209        String pageTitle = page.getTitle();
210        String translatedPageTitle = translatedPage.getTitle();
211        String pageUrl = getPageUrl(page);
212        String translatedPageUrl = getPageUrl(translatedPage);
213        
214        for (String language : recipientsByLanguage.keySet())
215        {
216            try
217            {
218                // Get a human-readable version of the languages.
219                String pageLang = _i18nUtils.translate(new I18nizableText("plugin.web", "I18NKEY_LANGUAGE_" + page.getSitemapName().toUpperCase()), language);
220                String translatedLang = _i18nUtils.translate(new I18nizableText("plugin.web", "I18NKEY_LANGUAGE_" + translatedPage.getSitemapName().toUpperCase()), language);
221                
222                // Build a list of the parameters.
223                List<String> params = new ArrayList<>();
224                params.add(siteTitle);
225                params.add(content.getTitle(LocaleUtils.toLocale(language)));
226                params.add(pageTitle);
227                params.add(pageLang.toLowerCase());
228                params.add(translatedPageTitle);
229                params.add(translatedLang.toLowerCase());
230                params.add(pageUrl);
231                params.add(translatedPageUrl);
232                
233                String catalogue = "plugin." + _pluginName;
234                
235                // Get the e-mail subject and body.
236                I18nizableText i18nSubject = new I18nizableText(catalogue, I18N_KEY_SUBJECT, params);
237                I18nizableText i18nBody = new I18nizableText(catalogue, I18N_KEY_BODY, params);
238                
239                String subject = _i18nUtils.translate(i18nSubject, language);
240                String htmlBody = StandardMailBodyHelper.newHTMLBody()
241                    .withTitle(new I18nizableText(catalogue, I18N_KEY_BODY_TITLE, params))
242                    .withMessage(i18nBody)
243                    .withLink(getPageUrl(translatedPage), new I18nizableText(catalogue, "PLUGINS_TRANSLATIONFLAGGING_ALERT_EMAIL_BODY_TRANSLATED_PAGE_LINK"))
244                    .withLanguage(language)
245                    .build();
246            
247                // Send the e-mails.
248                sendMails(subject, htmlBody, recipientsByLanguage.get(language), mailFrom);
249            }
250            catch (IOException e)
251            {
252                _logger.error("Unable to build HTML body for email alert on translation", e);
253            }
254        }
255    }
256    
257    /**
258     * Send a translation alert e-mail to the specified users.
259     * @param subject the e-mail subject.
260     * @param htmlBody the e-mail body.
261     * @param recipients the users' emails to send the e-mail to.
262     * @param from the e-mail will be sent with this "from" header.
263     */
264    protected void sendMails(String subject, String htmlBody, List<String> recipients, String from)
265    {
266        try
267        {
268            SendMailHelper.newMail()
269                          .withSubject(subject)
270                          .withHTMLBody(htmlBody)
271                          .withSender(from)
272                          .withRecipients(recipients)
273                          .sendMail();
274        }
275        catch (MessagingException | IOException e)
276        {
277            if (_logger.isWarnEnabled())
278            {
279                _logger.warn("Could not send a translation alert e-mail to " + recipients, e);
280            }
281        }
282    }
283    
284    /**
285     * Get the users to notify about the page translation.
286     * @param translatedPage the translated version of the page.
287     * @return the logins of the users to notify.
288     */
289    protected HashSet<UserIdentity> getUsersToNotify(Page translatedPage)
290    {
291        HashSet<UserIdentity> users = new HashSet<>();
292        
293        // Get the users which have the right to modify the page AND to receive the notification.
294        Set<UserIdentity> editors = _rightManager.getAllowedUsers("Workflow_Rights_Edition_Online", translatedPage).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"));
295        Set<UserIdentity> usersToNotify = _rightManager.getAllowedUsers("TranslationFlagging_Rights_Notification", translatedPage).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"));
296        
297        users.addAll(editors);
298        users.retainAll(usersToNotify);
299        
300        return users;
301    }
302    
303    /**
304     * Get the translations of a given page.
305     * @param page the page.
306     * @return the translated pages as a Map of pages, indexed by sitemap name (language).
307     */
308    protected Map<String, Page> getTranslations(Page page)
309    {
310        Map<String, Page> translations = new HashMap<>();
311        
312        ModelLessDataHolder translationsComposite = page.getComposite(TranslationFlaggingClientSideElement.TRANSLATIONS_META);
313        
314        if (translationsComposite != null)
315        {
316            for (String lang : translationsComposite.getDataNames())
317            {
318                String translatedPageId = translationsComposite.getValue(lang);
319                Page translatedPage = _resolver.resolveById(translatedPageId);
320                
321                translations.put(lang, translatedPage);
322            }
323        }
324        else
325        {
326            // Ignore : the translations composite data doesn't exist, just return an empty map.
327        }
328        
329        return translations;
330    }
331    
332    /**
333     * Get the URL of the back-office, opening on the page tool.
334     * @param page the page to open on.
335     * @return the page URL.
336     */
337    protected String getPageUrl(Page page)
338    {
339        StringBuilder url = new StringBuilder(_baseUrl);
340        url.append(page.getSite().getName()).append("/index.html?uitool=uitool-page,id:%27").append(page.getId()).append("%27");
341        return url.toString();
342    }
343
344    public I18nizableText getLabel()
345    {
346        return new I18nizableText("plugin.translationflagging", "PLUGINS_TRANSLATIONFLAGGING_ALERT_FUNCTION_LABEL");
347    }
348    
349}