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}