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