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