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.ByteArrayOutputStream; 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.FileOutputStream; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.OutputStream; 025import java.util.HashMap; 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.components.source.impl.SitemapSource; 033import org.apache.commons.io.FileUtils; 034import org.apache.commons.lang.StringUtils; 035import org.apache.commons.lang.math.NumberUtils; 036import org.apache.excalibur.source.Source; 037import org.apache.excalibur.source.SourceResolver; 038import org.apache.excalibur.source.SourceUtil; 039import org.apache.excalibur.xml.sax.SAXParser; 040import org.quartz.JobExecutionContext; 041import org.xml.sax.Attributes; 042import org.xml.sax.InputSource; 043import org.xml.sax.SAXException; 044import org.xml.sax.helpers.DefaultHandler; 045 046import org.ametys.core.right.RightManager; 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.plugins.core.impl.schedule.AbstractStaticSchedulable; 053import org.ametys.plugins.repository.AmetysObjectResolver; 054import org.ametys.runtime.config.Config; 055import org.ametys.runtime.i18n.I18nizableText; 056import org.ametys.runtime.servlet.RuntimeConfig; 057 058import jakarta.mail.MessagingException; 059 060/** 061 * Content consistency schedulable: generate consistency information for all contents. 062 * Sends a report e-mail if there are inconsistencies. 063 */ 064public class CheckContentConsistencySchedulable extends AbstractStaticSchedulable implements Initializable 065{ 066 /** The report e-mail will be sent to users who possess this right on the application context. */ 067 protected static final String _MAIL_RIGHT = "CMS_Rights_ReceiveConsistencyReport"; 068 069 /** The server base URL. */ 070 protected String _baseUrl; 071 072 /** The report directory. */ 073 protected File _reportDirectory; 074 075 /** The ametys object resolver. */ 076 protected AmetysObjectResolver _ametysResolver; 077 078 /** The avalon source resolver. */ 079 protected SourceResolver _sourceResolver; 080 081 /** The rights manager. */ 082 protected RightManager _rightManager; 083 084 /** The i18n utils. */ 085 protected I18nUtils _i18nUtils; 086 087 /** The content of "from" field in emails. */ 088 protected String _mailFrom; 089 090 @Override 091 public void service(ServiceManager manager) throws ServiceException 092 { 093 super.service(manager); 094 095 _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 096 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 097 098 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 099 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 100 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 101 } 102 103 public void initialize() throws Exception 104 { 105 _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/"); 106 _mailFrom = Config.getInstance().getValue("smtp.mail.from"); 107 _reportDirectory = new File(RuntimeConfig.getInstance().getAmetysHome(), "consistency"); 108 } 109 110 @Override 111 public void execute(JobExecutionContext context) throws Exception 112 { 113 _generateReport(); 114 } 115 116 /** 117 * Generate the full consistency report. 118 * @throws IOException if an i/o error occurs. 119 */ 120 protected void _generateReport() throws IOException 121 { 122 SitemapSource source = null; 123 File reportTmpFile = null; 124 125 try 126 { 127 // Create the directory if it does not exist. 128 File reportDirectory = _getReportDirectory(); 129 FileUtils.forceMkdir(reportDirectory); 130 131 // Resolve the report pipeline. 132 String url = _getReportURL(); 133 source = (SitemapSource) _sourceResolver.resolveURI(url); 134 135 // Save the report into a temporary file. 136 reportTmpFile = new File(reportDirectory, "report.tmp.xml"); 137 OutputStream reportTmpOs = new FileOutputStream(reportTmpFile); 138 139 SourceUtil.copy(source.getInputStream(), reportTmpOs); 140 141 // If all went well until now, copy the temporary file to the real report file. 142 File reportFile = new File(reportDirectory, "report.xml"); 143 FileUtils.copyFile(reportTmpFile, reportFile); 144 145 SAXParser saxParser = null; 146 try (FileInputStream reportIs = new FileInputStream(reportFile)) 147 { 148 // Parse the report to know if there were contents with inconsistencies. 149 ContentExistsHandler handler = new ContentExistsHandler(); 150 saxParser = (SAXParser) _smanager.lookup(SAXParser.ROLE); 151 saxParser.parse(new InputSource(reportIs), handler); 152 153 // If inconsistent contents exist, send an e-mail. 154 if (handler.hasFailures()) 155 { 156 _sendErrorEmail(); 157 } 158 } 159 finally 160 { 161 _smanager.release(saxParser); 162 } 163 } 164 catch (ServiceException e) 165 { 166 getLogger().error("Unable to get a SAX parser.", e); 167 } 168 catch (SAXException e) 169 { 170 getLogger().error("The consistency report could not be parsed.", e); 171 } 172 finally 173 { 174 // Delete the temporary file. 175 if (reportTmpFile != null) 176 { 177 reportTmpFile.delete(); 178 } 179 180 if (source != null) 181 { 182 _sourceResolver.release(source); 183 } 184 } 185 } 186 187 /** 188 * Retrieves the directory where to generate the report 189 * @return the directory where to generate the report 190 */ 191 protected File _getReportDirectory() 192 { 193 return _reportDirectory; 194 } 195 196 /** 197 * Retrieves the URL of the source to resolve to generate the report 198 * @return the URL of the source to resolve to generate the report 199 */ 200 protected String _getReportURL() 201 { 202 return "cocoon://_plugins/cms/consistency/inconsistent-contents-report.xml"; 203 } 204 205 /** 206 * Send a reminder e-mail to all the users who have the right to edit. 207 * @throws IOException if an error occurs building or sending the mail. 208 */ 209 protected void _sendErrorEmail() throws IOException 210 { 211 Set<UserIdentity> users = _rightManager.getAllowedUsers(_MAIL_RIGHT, "/cms").resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")); 212 213 Map<String, String> params = _getEmailParams(); 214 215 I18nizableText i18nSubject = _getMailSubject(params); 216 String subject = _i18nUtils.translate(i18nSubject); 217 218 String body = _getMailPart(params); 219 220 if (StringUtils.isNotEmpty(body)) 221 { 222 _sendMails(subject, body, users, _mailFrom); 223 } 224 } 225 226 /** 227 * Retrieves the mail's subject 228 * @param parameters the mail parameters. 229 * @return the mail's subject 230 */ 231 protected I18nizableText _getMailSubject(Map<String, String> parameters) 232 { 233 return new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_SUBJECT"); 234 } 235 236 /** 237 * Get a mail part. 238 * @param parameters the mail parameters. 239 * @return the mail part. 240 * @throws IOException if an error occurred 241 */ 242 protected String _getMailPart(Map<String, String> parameters) throws IOException 243 { 244 Source source = null; 245 InputStream is = null; 246 try 247 { 248 String uri = _getMailUri(parameters); 249 source = _sourceResolver.resolveURI(uri, null, parameters); 250 is = source.getInputStream(); 251 252 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 253 SourceUtil.copy(is, bos); 254 255 return bos.toString("UTF-8"); 256 } 257 finally 258 { 259 if (is != null) 260 { 261 is.close(); 262 } 263 264 if (source != null) 265 { 266 _sourceResolver.release(source); 267 } 268 } 269 } 270 271 /** 272 * Get the pipeline uri for mail body 273 * @param parameters the mail paramters 274 * @return a pipeline uri 275 */ 276 protected String _getMailUri (Map<String, String> parameters) 277 { 278 return "cocoon://_plugins/cms/consistency/inconsistent-contents-mail.html"; 279 } 280 281 /** 282 * Get the report e-mail parameters. 283 * @return the e-mail parameters. 284 */ 285 protected Map<String, String> _getEmailParams() 286 { 287 Map<String, String> params = new HashMap<>(); 288 289 StringBuilder url = new StringBuilder(_baseUrl); 290 url.append("/index.html?uitool=uitool-global-consistency"); 291 292 params.put("url", url.toString()); 293 294 return params; 295 } 296 297 /** 298 * Send the alert emails. 299 * @param subject the e-mail subject. 300 * @param body the e-mail body. 301 * @param users users to send the mail to. 302 * @param from the address sending the e-mail. 303 */ 304 protected void _sendMails(String subject, String body, Set<UserIdentity> users, String from) 305 { 306 for (UserIdentity userIdentity : users) 307 { 308 User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin()); 309 310 if (user != null && StringUtils.isNotBlank(user.getEmail())) 311 { 312 String mail = user.getEmail(); 313 314 try 315 { 316 SendMailHelper.newMail() 317 .withSubject(subject) 318 .withTextBody(body) 319 .withSender(from) 320 .withRecipient(mail) 321 .sendMail(); 322 } 323 catch (MessagingException | IOException e) 324 { 325 if (getLogger().isWarnEnabled()) 326 { 327 getLogger().warn("Could not send an alert e-mail to " + mail, e); 328 } 329 } 330 } 331 } 332 } 333 334 /** 335 * Handler which tests if exists a "/contents/content" tag. 336 */ 337 protected class ContentExistsHandler extends DefaultHandler 338 { 339 340 /** In content tag? */ 341 protected boolean _inContentsTag; 342 343 /** Has content tag? */ 344 protected boolean _hasContent; 345 346 /** True if the report has content with failures. */ 347 protected boolean _hasFailures; 348 349 /** 350 * Create a handler. 351 */ 352 public ContentExistsHandler() 353 { 354 super(); 355 } 356 357 @Override 358 public void startDocument() throws SAXException 359 { 360 super.startDocument(); 361 _inContentsTag = false; 362 _hasContent = false; 363 _hasFailures = false; 364 } 365 366 @Override 367 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException 368 { 369 super.startElement(uri, localName, qName, attributes); 370 if ("contents".equals(localName)) 371 { 372 _inContentsTag = true; 373 } 374 else if (_inContentsTag && "content".equals(localName)) 375 { 376 _hasContent = true; 377 378 String notFoundCount = attributes.getValue("not-found-count"); 379 String unauthorizedCount = attributes.getValue("unauthorized-count"); 380 String serverErrorCount = attributes.getValue("server-error-count"); 381 if (NumberUtils.toInt(notFoundCount, -1) > 0 || NumberUtils.toInt(unauthorizedCount, -1) > 0 || NumberUtils.toInt(serverErrorCount, -1) > 0) 382 { 383 _hasFailures = true; 384 } 385 } 386 } 387 388 @Override 389 public void endElement(String uri, String localName, String qName) throws SAXException 390 { 391 if ("contents".equals(localName)) 392 { 393 _inContentsTag = false; 394 } 395 super.endElement(uri, localName, qName); 396 } 397 398 /** 399 * Has content. 400 * @return true if the XML file has a content, false otherwise. 401 */ 402 public boolean hasContent() 403 { 404 return _hasContent; 405 } 406 407 /** 408 * Has failures. 409 * @return true if the XML file has at least a content with failures, false otherwise. 410 */ 411 public boolean hasFailures() 412 { 413 return _hasFailures; 414 } 415 } 416 417}