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