001/* 002 * Copyright 2010 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.cms.content.consistency; 017 018import java.io.ByteArrayInputStream; 019import java.io.ByteArrayOutputStream; 020import java.io.File; 021import java.io.IOException; 022import java.io.InputStream; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 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.cocoon.ProcessingException; 034import org.apache.cocoon.components.ContextHelper; 035import org.apache.cocoon.environment.Request; 036import org.apache.commons.io.IOUtils; 037import org.apache.commons.lang3.StringUtils; 038import org.apache.excalibur.source.Source; 039import org.apache.excalibur.source.SourceResolver; 040import org.quartz.JobExecutionContext; 041 042import org.ametys.cms.content.consistency.ContentConsistencyManager.ConsistenciesReport; 043import org.ametys.core.right.RightManager; 044import org.ametys.core.schedule.progression.ContainerProgressionTracker; 045import org.ametys.core.schedule.progression.SimpleProgressionTracker; 046import org.ametys.core.ui.mail.StandardMailBodyHelper; 047import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder; 048import org.ametys.core.user.User; 049import org.ametys.core.user.UserIdentity; 050import org.ametys.core.user.UserManager; 051import org.ametys.core.util.HttpUtils; 052import org.ametys.core.util.I18nUtils; 053import org.ametys.core.util.mail.SendMailHelper; 054import org.ametys.core.util.mail.SendMailHelper.NamedStream; 055import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable; 056import org.ametys.plugins.repository.AmetysObjectResolver; 057import org.ametys.runtime.config.Config; 058import org.ametys.runtime.i18n.I18nizableText; 059import org.ametys.runtime.servlet.RuntimeConfig; 060 061import jakarta.mail.MessagingException; 062 063/** 064 * Content consistency schedulable: generate consistency information for all contents. 065 * Sends a report e-mail if there are inconsistencies. 066 */ 067public class CheckContentConsistencySchedulable extends AbstractStaticSchedulable implements Initializable 068{ 069 /** Id of progression tracker for removing outdated results*/ 070 public static final String REMOVE_OUTDATED_RESULT = "remove-outdated-result"; 071 /** Id of progression tracker for removing outdated results*/ 072 public static final String CHECK_CONTENTS = "check-contents"; 073 /** The report e-mail will be sent to users who possess this right on the application context. */ 074 protected static final String __MAIL_RIGHT = "CMS_Rights_ReceiveConsistencyReport"; 075 /** Id of progression tracker for notifications */ 076 protected static final String __CHECK_CONTENT_CONSISTENCY_NOTIFICATION = "check-content-consistency-notification"; 077 /** Id of progression tracker for consistensy report */ 078 protected static final String __CHECK_CONTENT_CONSISTENCY_REPORT = "check-content-consistency-report"; 079 080 /** The server base URL. */ 081 protected String _baseUrl; 082 083 /** The report directory. */ 084 protected File _reportDirectory; 085 086 /** The ametys object resolver. */ 087 protected AmetysObjectResolver _ametysResolver; 088 089 /** The avalon source resolver. */ 090 protected SourceResolver _sourceResolver; 091 092 /** The rights manager. */ 093 protected RightManager _rightManager; 094 095 /** The i18n utils. */ 096 protected I18nUtils _i18nUtils; 097 098 /** The content of "from" field in emails. */ 099 protected String _mailFrom; 100 101 /** The content consistency manager */ 102 protected ContentConsistencyManager _contentConsistencyManager; 103 104 private ContentConsistencySearchModel _contentConsistencySearchModel; 105 106 @Override 107 public void service(ServiceManager manager) throws ServiceException 108 { 109 super.service(manager); 110 111 _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 112 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 113 114 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 115 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 116 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 117 118 _contentConsistencySearchModel = (ContentConsistencySearchModel) manager.lookup(ContentConsistencySearchModel.ROLE); 119 _contentConsistencyManager = (ContentConsistencyManager) manager.lookup(ContentConsistencyManager.ROLE); 120 } 121 122 public void initialize() throws Exception 123 { 124 _baseUrl = HttpUtils.sanitize(Config.getInstance().getValue("cms.url")); 125 _mailFrom = Config.getInstance().getValue("smtp.mail.from"); 126 _reportDirectory = new File(RuntimeConfig.getInstance().getAmetysHome(), "consistency"); 127 } 128 129 @Override 130 public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception 131 { 132 // Step's weight has been arbitrarily chosen because creating report is way longer than sending emails 133 ContainerProgressionTracker reportStep = progressionTracker.addContainerStep(__CHECK_CONTENT_CONSISTENCY_REPORT, new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_REPORT_STEP_LABEL"), 10); 134 reportStep.addSimpleStep(CHECK_CONTENTS, new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_CHECK_CONTENTS_LABEL")); 135 reportStep.addSimpleStep(REMOVE_OUTDATED_RESULT, new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_REMOVE_OUTDATED_RESULT_LABEL")); 136 SimpleProgressionTracker sendEmailStep = progressionTracker.addSimpleStep(__CHECK_CONTENT_CONSISTENCY_NOTIFICATION, new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_NOTIFICATION_STEPLABEL")); 137 138 ConsistenciesReport report = _contentConsistencyManager.checkAllContents(reportStep); 139 140 Set<UserIdentity> users = getUsersToNotify(); 141 if (!report.isEmpty() && !users.isEmpty()) 142 { 143 sendEmailStep.setSize(users.size()); 144 _sendEmail(report, users, sendEmailStep); 145 } 146 } 147 148 /** 149 * Send a reminder e-mail to all the users who have the right to edit. 150 * @param report the consistency report 151 * @param users the users to notify 152 * @param progressionTracker the progression tracker for sending email 153 * @throws IOException if an error occurs building or sending the mail. 154 * @throws ProcessingException if an error occurs 155 */ 156 protected void _sendEmail(ConsistenciesReport report, Set<UserIdentity> users, SimpleProgressionTracker progressionTracker) throws IOException, ProcessingException 157 { 158 I18nizableText i18nSubject = _getMailSubject(report); 159 MailBodyBuilder body = _getMailBody(report); 160 161 try (InputStream attachment = _getReport(report.results())) 162 { 163 _sendMails(i18nSubject, body, users, _mailFrom, attachment, progressionTracker); 164 } 165 } 166 167 /** 168 * Compute the list of user to notify 169 * @return a list of user identity 170 */ 171 protected Set<UserIdentity> getUsersToNotify() 172 { 173 Set<UserIdentity> users = _rightManager.getAllowedUsers(__MAIL_RIGHT, "/cms").resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")); 174 return users; 175 } 176 177 private InputStream _getReport(List<String> resultsId) throws ProcessingException 178 { 179 Request request = ContextHelper.getRequest(_context); 180 Source attachement = null; 181 try 182 { 183 184 List<Map<String, Object>> results = new ArrayList<>(); 185 for (String resultId : resultsId) 186 { 187 ContentConsistencyResult result = _ametysResolver.resolveById(resultId); 188 List<Map<String, Object>> columns = _contentConsistencySearchModel.getColumns(); 189 results.add(_contentConsistencyManager.resultToJSON(result, columns)); 190 } 191 request.setAttribute(ContentConsistencyResultGenerator.RESULTS_REQUEST_ATTRIBUTE_NAME, results); 192 193 attachement = _sourceResolver.resolveURI("cocoon://_plugins/cms/consistency/report.csv"); 194 InputStream attachmentCopy = null; 195 // Get the attachment and store it in a reusable inputStream. 196 // If the attachment size exceed 1M we won't attach it 197 try (InputStream is = attachement.getInputStream()) 198 { 199 // Override the byteArray to avoid duplication of buffer during copy 200 ByteArrayOutputStream tmp = new ByteArrayOutputStream() 201 { 202 @Override 203 public synchronized byte[] toByteArray() 204 { 205 return buf; 206 } 207 }; 208 IOUtils.copyLarge(is, tmp, 0, 1_000_000); 209 if (is.read() == -1) 210 { 211 // We could read all the stream so the size is acceptable and we keep the attachment 212 attachmentCopy = new ByteArrayInputStream(tmp.toByteArray()); 213 } 214 215 } 216 catch (IOException e) 217 { 218 getLogger().error("Failed to retrieve consistency report", e); 219 } 220 return attachmentCopy; 221 } 222 catch (IOException e) 223 { 224 getLogger().error("Failed to retrieve report", e); 225 return null; 226 } 227 finally 228 { 229 _sourceResolver.release(attachement); 230 request.removeAttribute(ContentConsistencyResultGenerator.RESULTS_REQUEST_ATTRIBUTE_NAME); 231 } 232 } 233 234 /** 235 * Retrieves the mail's subject 236 * @param report the consistency report 237 * @return the mail's subject 238 */ 239 protected I18nizableText _getMailSubject(ConsistenciesReport report) 240 { 241 return new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_SUBJECT"); 242 } 243 244 /** 245 * Retrieves the mail's body 246 * @param report the consistency report 247 * @return the mail's body 248 */ 249 protected MailBodyBuilder _getMailBody(ConsistenciesReport report) 250 { 251 MailBodyBuilder mailBuilder = StandardMailBodyHelper.newHTMLBody() 252 .withTitle(_getMailSubject(report)); 253 254 int nbFailure = report.results() != null ? report.results().size() : 0; 255 256 if (nbFailure == 1) 257 { 258 mailBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_FAILURE")); 259 } 260 else if (nbFailure > 1) 261 { 262 mailBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_FAILURES", List.of(String.valueOf(nbFailure)))); 263 } 264 265 if (!(report.unchecked() == null || report.unchecked().isEmpty())) 266 { 267 mailBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_UNCHECKED", List.of(StringUtils.join(report.unchecked(), ", ")))); 268 } 269 270 mailBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_REPORT", List.of(_getReportUri()))); 271 272 mailBuilder.withLink(_getReportUri(), new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_REPORT_LINK_TITLE")); 273 274 return mailBuilder; 275 } 276 277 /** 278 * Get the report URI 279 * @return the report uri 280 */ 281 protected String _getReportUri() 282 { 283 StringBuilder url = new StringBuilder(_baseUrl); 284 url.append("/index.html?uitool=uitool-global-consistency"); 285 return url.toString(); 286 } 287 288 /** 289 * Send the alert e-mails. 290 * @param i18nSubject the e-mail subject. 291 * @param bodyBuilder the e-mail body. 292 * @param users users to send the mail to. 293 * @param from the address sending the e-mail. 294 * @param attachment the e-mail attachment 295 * @param progressionTracker progression tracker for sending report mail 296 */ 297 protected void _sendMails(I18nizableText i18nSubject, MailBodyBuilder bodyBuilder, Set<UserIdentity> users, String from, InputStream attachment, SimpleProgressionTracker progressionTracker) 298 { 299 Map<String, String> bodyByLanguage = new HashMap<>(); 300 301 for (UserIdentity userIdentity : users) 302 { 303 User user = _userManager.getUser(userIdentity); 304 305 if (user != null && StringUtils.isNotBlank(user.getEmail())) 306 { 307 String language = StringUtils.defaultIfBlank(user.getLanguage(), _userLanguagesManager.getDefaultLanguage()); 308 309 String subject = _i18nUtils.translate(i18nSubject, language); 310 String body = bodyByLanguage.computeIfAbsent(language, lang -> { 311 try 312 { 313 return bodyBuilder.withLanguage(lang).build(); 314 } 315 catch (IOException e) 316 { 317 getLogger().error("Fail to build HTML content consistency mail report", e); 318 return null; 319 } 320 }); 321 322 String email = user.getEmail(); 323 324 Collection<NamedStream> attachments = null; 325 if (attachment != null) 326 { 327 try 328 { 329 attachment.reset(); 330 } 331 catch (IOException e) 332 { 333 // in case reset is not implemented. 334 } 335 attachments = List.of(new NamedStream(attachment, "report.csv", "text/csv")); 336 } 337 338 if (body != null) 339 { 340 try 341 { 342 SendMailHelper.newMail() 343 .withSubject(subject) 344 .withHTMLBody(body) 345 .withSender(from) 346 .withRecipient(email) 347 .withAttachmentsAsStream(attachments) 348 .sendMail(); 349 } 350 catch (MessagingException | IOException e) 351 { 352 if (getLogger().isWarnEnabled()) 353 { 354 getLogger().warn("Could not send an alert e-mail to " + email, e); 355 } 356 } 357 } 358 } 359 progressionTracker.increment(); 360 } 361 } 362}