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