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.ArrayList; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030 031import javax.mail.MessagingException; 032 033import org.apache.avalon.framework.configuration.Configuration; 034import org.apache.avalon.framework.configuration.ConfigurationException; 035import org.apache.avalon.framework.context.Context; 036import org.apache.avalon.framework.context.ContextException; 037import org.apache.avalon.framework.service.ServiceException; 038import org.apache.avalon.framework.service.ServiceManager; 039import org.apache.cocoon.Constants; 040import org.apache.cocoon.components.ContextHelper; 041import org.apache.cocoon.components.source.impl.SitemapSource; 042import org.apache.cocoon.environment.ObjectModelHelper; 043import org.apache.cocoon.environment.Request; 044import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 045import org.apache.commons.io.FileUtils; 046import org.apache.commons.lang.StringUtils; 047import org.apache.commons.lang.math.NumberUtils; 048import org.apache.excalibur.source.Source; 049import org.apache.excalibur.source.SourceResolver; 050import org.apache.excalibur.source.SourceUtil; 051import org.apache.excalibur.xml.sax.SAXParser; 052import org.slf4j.Logger; 053import org.slf4j.LoggerFactory; 054import org.xml.sax.Attributes; 055import org.xml.sax.InputSource; 056import org.xml.sax.SAXException; 057import org.xml.sax.helpers.DefaultHandler; 058 059import org.ametys.core.authentication.AuthenticateAction; 060import org.ametys.core.engine.BackgroundEngineHelper; 061import org.ametys.core.engine.BackgroundEnvironment; 062import org.ametys.core.right.RightManager; 063import org.ametys.core.user.User; 064import org.ametys.core.user.UserIdentity; 065import org.ametys.core.user.UserManager; 066import org.ametys.core.user.population.PopulationContextHelper; 067import org.ametys.core.util.I18nUtils; 068import org.ametys.core.util.mail.SendMailHelper; 069import org.ametys.plugins.repository.AmetysObjectResolver; 070import org.ametys.plugins.repository.AmetysRepositoryException; 071import org.ametys.runtime.config.Config; 072import org.ametys.runtime.i18n.I18nizableText; 073import org.ametys.runtime.servlet.RuntimeConfig; 074 075/** 076 * Content consistency engine: generate consistency information for all contents. 077 * Sends a report e-mail if there are inconsistencies. 078 */ 079public class ContentConsistencyEngine implements Runnable 080{ 081 082 /** The logger. */ 083 protected static final Logger _LOGGER = LoggerFactory.getLogger(ContentConsistencyEngine.class); 084 085 /** The report e-mail will be sent to users who possess this right on the application context. */ 086 protected static final String _MAIL_RIGHT = "CMS_Rights_ReceiveConsistencyReport"; 087 088 private static boolean _RUNNING; 089 090 /** The avalon context. */ 091 protected Context _context; 092 093 /** The service manager. */ 094 protected ServiceManager _manager; 095 096 /** The server base URL. */ 097 protected String _baseUrl; 098 099 /** The report directory. */ 100 protected File _reportDirectory; 101 102 /** Is the engine initialized ? */ 103 protected boolean _initialized; 104 105 /** The cocoon environment context. */ 106 protected org.apache.cocoon.environment.Context _environmentContext; 107 108 /** The ametys object resolver. */ 109 protected AmetysObjectResolver _ametysResolver; 110 111 /** The avalon source resolver. */ 112 protected SourceResolver _sourceResolver; 113 114 /** The rights manager. */ 115 protected RightManager _rightManager; 116 117 /** The users manager. */ 118 protected UserManager _userManager; 119 120 /** The i18n utils. */ 121 protected I18nUtils _i18nUtils; 122 123 /** The content of "from" field in emails. */ 124 protected String _mailFrom; 125 126 /** 127 * Initialize the alert engine. 128 * @param manager the avalon service manager. 129 * @param context the avalon context. 130 * @throws ContextException if an error occurs retrieving the environment context. 131 * @throws ServiceException if an error occurs retrieving a component. 132 */ 133 public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException 134 { 135 _manager = manager; 136 _context = context; 137 _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 138 139 // Lookup the needed components. 140 _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 141 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 142 143 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 144 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 145 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 146 147 _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/"); 148 _mailFrom = Config.getInstance().getValue("smtp.mail.from"); 149 _reportDirectory = new File(RuntimeConfig.getInstance().getAmetysHome(), "consistency"); 150 151 _initialized = true; 152 } 153 154 /** 155 * Configure the engine (called by the scheduler). 156 * @param configuration the component configuration. 157 * @throws ConfigurationException if an error occurred 158 */ 159 public void configure(Configuration configuration) throws ConfigurationException 160 { 161 // Ignore 162 } 163 164 /** 165 * Check the initialization and throw an exception if not initialized. 166 */ 167 protected void _checkInitialization() 168 { 169 if (!_initialized) 170 { 171 String message = "Le composant de synchronisation doit être initialisé avant d'être lancé."; 172 _LOGGER.error(message); 173 throw new IllegalStateException(message); 174 } 175 } 176 177 /** 178 * Test if the engine is running at the time. 179 * @return true if the engine is running, false otherwise. 180 */ 181 static boolean isRunning() 182 { 183 return _RUNNING; 184 } 185 186 private static void setRunning(boolean running) 187 { 188 _RUNNING = running; 189 } 190 191 @Override 192 public void run() 193 { 194 Map<String, Object> environmentInformation = null; 195 196 try 197 { 198 _LOGGER.info("Preparing to generate the content consistency report..."); 199 200 _checkInitialization(); 201 202 // Create the environment. 203 environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_manager, _environmentContext, new SLF4JLoggerAdapter(_LOGGER)); 204 BackgroundEnvironment environment = (BackgroundEnvironment) environmentInformation.get("environment"); 205 206 // Authorize workflow actions and "check-auth" CMS action, from this background environment 207 Request request = (Request) environment.getObjectModel().get(ObjectModelHelper.REQUEST_OBJECT); 208 request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED, true); 209 210 _generateReports(); 211 } 212 catch (Exception e) 213 { 214 _LOGGER.error("An error occurred generating the content consistency report.", e); 215 } 216 finally 217 { 218 // Leave the environment. 219 if (environmentInformation != null) 220 { 221 BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation); 222 } 223 // Dispose of the resources. 224 _dispose(); 225 _LOGGER.info("Content consistency report generated."); 226 } 227 } 228 229 /** 230 * Dispose of the resources and looked-up components. 231 */ 232 protected void _dispose() 233 { 234 // Release the components. 235 if (_manager != null) 236 { 237 _manager.release(_ametysResolver); 238 _manager.release(_rightManager); 239 _manager.release(_userManager); 240 } 241 242 _ametysResolver = null; 243 _sourceResolver = null; 244 _rightManager = null; 245 _userManager = null; 246 247 _environmentContext = null; 248 _context = null; 249 _manager = null; 250 251 _initialized = false; 252 } 253 254 /** 255 * Send all the alerts. Can be overridden to add alerts. 256 * @throws AmetysRepositoryException if an error occurs. 257 * @throws IOException if an error occurred 258 */ 259 protected void _generateReports() throws AmetysRepositoryException, IOException 260 { 261 if (isRunning()) 262 { 263 _LOGGER.error("Cannot start a global consistency check, as the engine is running at the time."); 264 } 265 else 266 { 267 try 268 { 269 setRunning(true); 270 271 Request request = ContextHelper.getRequest(_context); 272 273 // Set the population contexts to be able to get allowed users 274 List<String> populationContexts = new ArrayList<>(); 275 populationContexts.add("/application"); 276 277 request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts); 278 279 // Generate the report. 280 _generateReport(); 281 } 282 finally 283 { 284 setRunning(false); 285 } 286 } 287 } 288 289 /** 290 * Generate the full consistency report. 291 * @throws IOException if an i/o error occurs. 292 */ 293 protected void _generateReport() throws IOException 294 { 295 SitemapSource source = null; 296 File reportTmpFile = null; 297 298 try 299 { 300 // Create the directory if it does not exist. 301 FileUtils.forceMkdir(_reportDirectory); 302 303 // Resolve the report pipeline. 304 String url = "cocoon://_plugins/cms/consistency/inconsistent-contents-report.xml"; 305 source = (SitemapSource) _sourceResolver.resolveURI(url); 306 307 // Save the report into a temporary file. 308 reportTmpFile = new File(_reportDirectory, "report.tmp.xml"); 309 OutputStream reportTmpOs = new FileOutputStream(reportTmpFile); 310 311 SourceUtil.copy(source.getInputStream(), reportTmpOs); 312 313 // If all went well until now, copy the temporary file to the real report file. 314 File reportFile = new File(_reportDirectory, "report.xml"); 315 FileUtils.copyFile(reportTmpFile, reportFile); 316 317 SAXParser saxParser = null; 318 try (FileInputStream reportIs = new FileInputStream(reportFile)) 319 { 320 // Parse the report to know if there were contents with inconsistencies. 321 ContentExistsHandler handler = new ContentExistsHandler(); 322 saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE); 323 saxParser.parse(new InputSource(reportIs), handler); 324 325 // If inconsistent contents exist, send an e-mail. 326 if (handler.hasFailures()) 327 { 328 _sendErrorEmail(); 329 } 330 } 331 finally 332 { 333 _manager.release(saxParser); 334 } 335 } 336 catch (ServiceException e) 337 { 338 _LOGGER.error("Unable to get a SAX parser.", e); 339 } 340 catch (SAXException e) 341 { 342 _LOGGER.error("The consistency report could not be parsed.", e); 343 } 344 finally 345 { 346 // Delete the temporary file. 347 if (reportTmpFile != null) 348 { 349 reportTmpFile.delete(); 350 } 351 352 if (source != null) 353 { 354 _sourceResolver.release(source); 355 } 356 } 357 } 358 359 /** 360 * Send a reminder e-mail to all the users who have the right to edit. 361 * @throws IOException if an error occurs building or sending the mail. 362 */ 363 protected void _sendErrorEmail() throws IOException 364 { 365 Set<UserIdentity> users = _rightManager.getAllowedUsers(_MAIL_RIGHT, "/cms").resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")); 366 367 Map<String, String> params = _getEmailParams(); 368 369 I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_SUBJECT"); 370 371 String subject = _i18nUtils.translate(i18nSubject); 372 String body = _getMailPart(params); 373 374 if (StringUtils.isNotEmpty(body)) 375 { 376 _sendMails(subject, body, users, _mailFrom); 377 } 378 } 379 380 /** 381 * Get a mail part. 382 * @param parameters the pipeline parameters. 383 * @return the mail part. 384 * @throws IOException if an error occurred 385 */ 386 protected String _getMailPart(Map<String, String> parameters) throws IOException 387 { 388 Source source = null; 389 InputStream is = null; 390 try 391 { 392 String uri = _getMailUri(parameters); 393 source = _sourceResolver.resolveURI(uri, null, parameters); 394 is = source.getInputStream(); 395 396 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 397 SourceUtil.copy(is, bos); 398 399 return bos.toString("UTF-8"); 400 } 401 finally 402 { 403 if (is != null) 404 { 405 is.close(); 406 } 407 408 if (source != null) 409 { 410 _sourceResolver.release(source); 411 } 412 } 413 } 414 415 /** 416 * Get the pipeline uri for mail body 417 * @param parameters the mail paramters 418 * @return a pipeline uri 419 */ 420 protected String _getMailUri (Map<String, String> parameters) 421 { 422 return "cocoon://_plugins/cms/consistency/inconsistent-contents-mail.html"; 423 } 424 425 /** 426 * Get the report e-mail parameters. 427 * @return the e-mail parameters. 428 */ 429 protected Map<String, String> _getEmailParams() 430 { 431 Map<String, String> params = new HashMap<>(); 432 433 StringBuilder url = new StringBuilder(_baseUrl); 434 url.append("/index.html?uitool=uitool-global-consistency"); 435 436 params.put("url", url.toString()); 437 438 return params; 439 } 440 441 /** 442 * Send the alert emails. 443 * @param subject the e-mail subject. 444 * @param body the e-mail body. 445 * @param users users to send the mail to. 446 * @param from the address sending the e-mail. 447 */ 448 protected void _sendMails(String subject, String body, Set<UserIdentity> users, String from) 449 { 450 for (UserIdentity userIdentity : users) 451 { 452 User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin()); 453 454 if (user != null && StringUtils.isNotBlank(user.getEmail())) 455 { 456 String mail = user.getEmail(); 457 458 try 459 { 460 SendMailHelper.sendMail(subject, null, body, mail, from); 461 } 462 catch (MessagingException e) 463 { 464 if (_LOGGER.isWarnEnabled()) 465 { 466 _LOGGER.warn("Could not send an alert e-mail to " + mail, e); 467 } 468 } 469 } 470 } 471 } 472 473 /** 474 * Handler which tests if exists a "/contents/content" tag. 475 */ 476 protected class ContentExistsHandler extends DefaultHandler 477 { 478 479 /** In content tag? */ 480 protected boolean _inContentsTag; 481 482 /** Has content tag? */ 483 protected boolean _hasContent; 484 485 /** True if the report has content with failures. */ 486 protected boolean _hasFailures; 487 488 /** 489 * Create a handler. 490 */ 491 public ContentExistsHandler() 492 { 493 super(); 494 } 495 496 @Override 497 public void startDocument() throws SAXException 498 { 499 super.startDocument(); 500 _inContentsTag = false; 501 _hasContent = false; 502 _hasFailures = false; 503 } 504 505 @Override 506 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException 507 { 508 super.startElement(uri, localName, qName, attributes); 509 if ("contents".equals(localName)) 510 { 511 _inContentsTag = true; 512 } 513 else if (_inContentsTag && "content".equals(localName)) 514 { 515 _hasContent = true; 516 517 String notFoundCount = attributes.getValue("not-found-count"); 518 String unauthorizedCount = attributes.getValue("unauthorized-count"); 519 String serverErrorCount = attributes.getValue("server-error-count"); 520 if (NumberUtils.toInt(notFoundCount, -1) > 0 || NumberUtils.toInt(unauthorizedCount, -1) > 0 || NumberUtils.toInt(serverErrorCount, -1) > 0) 521 { 522 _hasFailures = true; 523 } 524 } 525 } 526 527 @Override 528 public void endElement(String uri, String localName, String qName) throws SAXException 529 { 530 if ("contents".equals(localName)) 531 { 532 _inContentsTag = false; 533 } 534 super.endElement(uri, localName, qName); 535 } 536 537 /** 538 * Has content. 539 * @return true if the XML file has a content, false otherwise. 540 */ 541 public boolean hasContent() 542 { 543 return _hasContent; 544 } 545 546 /** 547 * Has failures. 548 * @return true if the XML file has at least a content with failures, false otherwise. 549 */ 550 public boolean hasFailures() 551 { 552 return _hasFailures; 553 } 554 555 } 556 557}