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}