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