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