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}