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