001/* 002 * Copyright 2012 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.workflow.archive; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.net.MalformedURLException; 021import java.text.DateFormat; 022import java.util.ArrayList; 023import java.util.Date; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029import org.apache.avalon.framework.configuration.Configuration; 030import org.apache.avalon.framework.configuration.ConfigurationException; 031import org.apache.avalon.framework.context.Context; 032import org.apache.avalon.framework.context.ContextException; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.cocoon.Constants; 036import org.apache.cocoon.environment.ObjectModelHelper; 037import org.apache.cocoon.environment.Request; 038import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 039import org.apache.commons.lang.StringUtils; 040import org.apache.excalibur.source.Source; 041import org.apache.excalibur.source.SourceResolver; 042import org.slf4j.Logger; 043import org.slf4j.LoggerFactory; 044 045import org.ametys.cms.content.archive.ArchiveConstants; 046import org.ametys.cms.repository.Content; 047import org.ametys.cms.repository.ContentQueryHelper; 048import org.ametys.core.authentication.AuthenticateAction; 049import org.ametys.core.engine.BackgroundEngineHelper; 050import org.ametys.core.engine.BackgroundEnvironment; 051import org.ametys.core.right.RightManager; 052import org.ametys.core.user.User; 053import org.ametys.core.user.UserIdentity; 054import org.ametys.core.user.UserManager; 055import org.ametys.core.user.population.PopulationContextHelper; 056import org.ametys.core.util.I18nUtils; 057import org.ametys.core.util.mail.SendMailHelper; 058import org.ametys.plugins.repository.AmetysObjectIterable; 059import org.ametys.plugins.repository.AmetysObjectResolver; 060import org.ametys.plugins.repository.AmetysRepositoryException; 061import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 062import org.ametys.plugins.repository.query.expression.DateExpression; 063import org.ametys.plugins.repository.query.expression.Expression; 064import org.ametys.plugins.repository.query.expression.Expression.Operator; 065import org.ametys.plugins.repository.query.expression.ExpressionContext; 066import org.ametys.runtime.config.Config; 067import org.ametys.runtime.i18n.I18nizableText; 068 069import jakarta.mail.MessagingException; 070 071/** 072 * Runnable engine that archive the contents that have an scheduled archiving 073 * date set before the current date. 074 */ 075public class ArchiveContentsEngine implements Runnable 076{ 077 /** The logger. */ 078 protected Logger _logger = LoggerFactory.getLogger(ArchiveContentsEngine.class); 079 080 /** The avalon context. */ 081 protected Context _context; 082 083 /** The service manager. */ 084 protected ServiceManager _manager; 085 086 /** The server base URL. */ 087 protected String _baseUrl; 088 089 /** Is the engine initialized ? */ 090 protected boolean _initialized; 091 092 /** The cocoon environment context. */ 093 protected org.apache.cocoon.environment.Context _environmentContext; 094 095 /** The ametys object resolver. */ 096 protected AmetysObjectResolver _ametysResolver; 097 098 /** The avalon source resolver. */ 099 protected SourceResolver _sourceResolver; 100 101 /** The rights manager */ 102 protected RightManager _rightManager; 103 104 /** The users manager. */ 105 protected UserManager _userManager; 106 107 /** The i18n utils. */ 108 protected I18nUtils _i18nUtils; 109 110 /** The content of "from" field in emails. */ 111 protected String _mailFrom; 112 113 /** The sysadmin mail address, to which will be sent the report e-mail. */ 114 protected String _sysadminMail; 115 116 /** The user e-mail notification will be sent to users that have this at least one of this rights. */ 117 protected Set<String> _archiveRights; 118 119 /** The user notification mail body i18n key. */ 120 protected String _userMailBody; 121 122 /** The user notification mail subject i18n key. */ 123 protected String _userMailSubject; 124 125 /** The user notification error mail body i18n key. */ 126 protected String _userErrorMailBody; 127 128 /** The user notification error mail subject i18n key. */ 129 protected String _userErrorMailSubject; 130 131 /** 132 * Initialize the archive engine. 133 * 134 * @param manager the avalon service manager. 135 * @param context the avalon context. 136 * @throws ContextException If an error occurred 137 * @throws ServiceException If an error occurred 138 */ 139 public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException 140 { 141 _manager = manager; 142 _context = context; 143 _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 144 145 // Lookup the needed components. 146 _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 147 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 148 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 149 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 150 151 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 152 153 _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/"); 154 _mailFrom = Config.getInstance().getValue("smtp.mail.from"); 155 _sysadminMail = Config.getInstance().getValue("smtp.mail.sysadminto"); 156 157 _initialized = true; 158 } 159 160 /** 161 * Configure the engine (called by the scheduler). 162 * 163 * @param configuration the component configuration. 164 * @throws ConfigurationException If an error occurred 165 */ 166 public void configure(Configuration configuration) throws ConfigurationException 167 { 168 // Rights 169 Configuration rightsConf = configuration.getChild("rights"); 170 171 _archiveRights = new HashSet<>(); 172 173 for (Configuration rightConf : rightsConf.getChildren("right")) 174 { 175 String right = rightConf.getValue(""); 176 if (StringUtils.isNotBlank(right)) 177 { 178 _archiveRights.add(right); 179 } 180 } 181 182 // Mails 183 Configuration validMailsConf = configuration.getChild("mails").getChild("valid"); 184 Configuration errorMailsConf = configuration.getChild("mails").getChild("error"); 185 186 _userMailBody = validMailsConf.getChild("bodyKey").getValue(); 187 _userMailSubject = validMailsConf.getChild("subjectKey").getValue(); 188 189 _userErrorMailBody = errorMailsConf.getChild("bodyKey").getValue(); 190 _userErrorMailSubject = errorMailsConf.getChild("subjectKey").getValue(); 191 192 } 193 194 /** 195 * Check the initialization and throw an exception if not initialized. 196 */ 197 protected void checkInitialization() 198 { 199 if (!_initialized) 200 { 201 String message = "The engine must be initialized before it can be runned."; 202 _logger.error(message); 203 throw new IllegalStateException(message); 204 } 205 } 206 207 @Override 208 public void run() 209 { 210 Map<String, Object> environmentInformation = null; 211 long duration = 0; 212 try 213 { 214 _logger.info("Preparing the scheduled archiving process on contents..."); 215 216 checkInitialization(); 217 218 // Create the environment. 219 environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_manager, _environmentContext, new SLF4JLoggerAdapter(_logger)); 220 BackgroundEnvironment environment = (BackgroundEnvironment) environmentInformation.get("environment"); 221 222 // Authorize workflow actions and "check-auth" CMS action, from this background environment 223 Request request = (Request) environment.getObjectModel().get(ObjectModelHelper.REQUEST_OBJECT); 224 request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED, true); 225 226 long start = System.currentTimeMillis(); 227 228 // Get all the needed contents and archive them. 229 archiveContents(request); 230 231 long end = System.currentTimeMillis(); 232 233 duration = (end - start) / 1000; 234 } 235 catch (Exception e) 236 { 237 _logger.error("An error occurred while archiving the contents.", e); 238 } 239 finally 240 { 241 // Leave the environment. 242 if (environmentInformation != null) 243 { 244 BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation); 245 } 246 247 // Dispose of the resources. 248 dispose(); 249 _logger.info("Scheduled archiving process ended after " + duration + " seconds."); 250 } 251 } 252 253 /** 254 * Dispose of the resources and looked-up components. 255 */ 256 protected void dispose() 257 { 258 // Release the components. 259 if (_manager != null) 260 { 261 _manager.release(_ametysResolver); 262 } 263 264 _ametysResolver = null; 265 _sourceResolver = null; 266 _userManager = null; 267 268 _i18nUtils = null; 269 270 _environmentContext = null; 271 _context = null; 272 _manager = null; 273 274 _initialized = false; 275 } 276 277 /** 278 * Get the contents that need to be archived, and archive them. 279 * @param request The current request object 280 * 281 * @throws AmetysRepositoryException if an error occurs. 282 * @throws IOException If an error occurred 283 * @throws MalformedURLException If an error occurred 284 */ 285 protected void archiveContents(Request request) throws AmetysRepositoryException, MalformedURLException, IOException 286 { 287 // Get all the content which scheduled archiving date is passed. 288 Expression dateExpression = new DateExpression(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE, Operator.LE, new Date(), ExpressionContext.newInstance().withUnversioned(true)); 289 String query = ContentQueryHelper.getContentXPathQuery(dateExpression); 290 291 try (AmetysObjectIterable<Content> contents = _ametysResolver.query(query)) 292 { 293 List<Content> archivedContents = new ArrayList<>(); 294 List<Content> contentsWithError = new ArrayList<>(); 295 296 try 297 { 298 for (Content content : contents) 299 { 300 setRequestAttributes(request, content); 301 302 Set<UserIdentity> users = _getAuthorizedContributors(content); 303 304 Source source = null; 305 try 306 { 307 String contentId = content.getId(); 308 309 String uri = getArchiveActionUri(contentId); 310 source = _sourceResolver.resolveURI(uri); 311 312 try (InputStream is = source.getInputStream()) 313 { 314 String workspaceBackup = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 315 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, ArchiveConstants.ARCHIVE_WORKSPACE); 316 317 Content archivedContent = _ametysResolver.resolveById(contentId); 318 archivedContents.add(archivedContent); 319 320 sendMailToContributors(archivedContent, users); 321 322 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceBackup); 323 } 324 } 325 catch (Exception e) 326 { 327 contentsWithError.add (content); 328 sendErrorMailToContributors(content, users); 329 _logger.error("Error while trying to archive the content : " + content.getId() + "\nThis content is probably not archived.", e); 330 } 331 finally 332 { 333 if (source != null) 334 { 335 _sourceResolver.release(source); 336 } 337 } 338 } 339 } 340 finally 341 { 342 // Send mail to administrator 343 sendMailToAdministrator(archivedContents, contentsWithError); 344 } 345 } 346 } 347 348 /** 349 * Set the necessary request attributes 350 * @param request The request 351 * @param content The content 352 */ 353 protected void setRequestAttributes (Request request, Content content) 354 { 355 // Set the population contexts to be able to get allowed users 356 List<String> populationContexts = new ArrayList<>(); 357 populationContexts.add("/application"); 358 359 request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts); 360 } 361 362 /** 363 * Get the pipeline uri for the archive action 364 * @param contentId the current contend id 365 * @return a pipeline uri 366 */ 367 protected String getArchiveActionUri(String contentId) 368 { 369 return "cocoon://_plugins/cms/archives/archive/" + ArchiveConstants.ARCHIVE_WORKFLOW_ACTION_ID + "?contentId=" + contentId; 370 } 371 372 /** 373 * Send the archive report e-mail. 374 * @param archivedContents The list of archived contents 375 * @param contentsWithError The list of contents with error 376 */ 377 protected void sendMailToAdministrator(List<Content> archivedContents, List<Content> contentsWithError) 378 { 379 if (archivedContents.size() != 0 || contentsWithError.size() != 0) 380 { 381 try 382 { 383 I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_ARCHIVE_CONTENTS_REPORT_SUBJECT"); 384 385 List<String> bodyParams = getAdminEmailParams(archivedContents, contentsWithError); 386 I18nizableText i18nBody = new I18nizableText("plugin.cms", "PLUGINS_CMS_ARCHIVE_CONTENTS_REPORT_BODY", bodyParams); 387 388 String subject = _i18nUtils.translate(i18nSubject); 389 String body = _i18nUtils.translate(i18nBody); 390 391 if (StringUtils.isNotBlank(_sysadminMail)) 392 { 393 SendMailHelper.newMail() 394 .withSubject(subject) 395 .withTextBody(body) 396 .withSender(_mailFrom) 397 .withRecipient(_sysadminMail) 398 .sendMail(); 399 } 400 } 401 catch (MessagingException | IOException e) 402 { 403 _logger.warn("Error sending the archive report e-mail.", e); 404 } 405 } 406 } 407 408 409 /** 410 * Get the report e-mail parameters. 411 * @param archivedContents The list of archived contents 412 * @param contentsWithError The list of contents with error 413 * @return the e-mail parameters. 414 */ 415 protected List<String> getAdminEmailParams(List<Content> archivedContents, List<Content> contentsWithError) 416 { 417 List<String> params = new ArrayList<>(); 418 419 DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT); 420 params.add(df.format(new Date())); // {0} 421 422 params.add(String.valueOf(archivedContents.size())); // {1} 423 params.add(_getContentsListAsString(archivedContents)); // {2} 424 425 params.add(String.valueOf(contentsWithError.size())); // {3} 426 params.add(_getContentsListAsString(contentsWithError)); // {4} 427 428 params.add(_getRequestURI(null) + "/index.html"); // {5} 429 430 return params; 431 } 432 433 /** 434 * Get the contents list as String 435 * @param contents The contents 436 * @return the list of contents as String 437 */ 438 protected String _getContentsListAsString (List<Content> contents) 439 { 440 List<String> contentNames = new ArrayList<>(); 441 for (Content content : contents) 442 { 443 contentNames.add("- " + content.getTitle(null)); 444 } 445 446 if (contentNames.size() > 0) 447 { 448 return StringUtils.join(contentNames, "\n"); 449 } 450 else 451 { 452 return ""; 453 } 454 } 455 456 /** 457 * Get the authorized contributors to receive mail notification 458 * @param content The content to be archived 459 * @return The user logins 460 */ 461 protected Set<UserIdentity> _getAuthorizedContributors (Content content) 462 { 463 Set<UserIdentity> users = new HashSet<>(); 464 for (String right : _archiveRights) 465 { 466 users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"))); 467 } 468 469 return users; 470 } 471 472 /** 473 * Send the mail to alert users that the content has been archived. 474 * @param content The archived content 475 * @param users The users 476 */ 477 protected void sendMailToContributors(Content content, Set<UserIdentity> users) 478 { 479 try 480 { 481 List<String> params = getBodyParamsForContributors (content, true); 482 483 I18nizableText i18nSubject = new I18nizableText(null, _userMailSubject, params); 484 I18nizableText i18nBody = new I18nizableText(null, _userMailBody, params); 485 486 String subject = _i18nUtils.translate(i18nSubject); 487 String body = _i18nUtils.translate(i18nBody); 488 489 _sendMailsToUsers(subject, body, users, _mailFrom); 490 } 491 catch (Exception e) 492 { 493 _logger.error("Unable to send mail to contributors after archiving content '" + content.getName() + "'", e); 494 } 495 } 496 497 /** 498 * Get email body parameters 499 * @param content the archived content 500 * @param archived true if the content has archived 501 * @return The mail parameters 502 */ 503 protected List<String> getBodyParamsForContributors (Content content, boolean archived) 504 { 505 List<String> params = new ArrayList<>(); 506 507 params.add(content.getTitle(null)); // {0} 508 params.add(DateFormat.getDateInstance(DateFormat.LONG).format(new Date())); // {1} 509 510 if (archived) 511 { 512 params.add(_getRequestURI(content) + "/index.html?uitool=uitool-content,id:%27" + content.getId() + "%27,workspace:%27archives%27,%27ignore-workflow%27:%27true%27,%27content-message-type%27:%27archived-content%27"); // {2} 513 } 514 else 515 { 516 params.add(_getRequestURI(content) + "/index.html?uitool=uitool-content,id:%27" + content.getId() + "%27"); 517 } 518 519 return params; 520 } 521 522 /** 523 * Send the mail to alert users that an error has occurred while trying to archive the content. 524 * @param content The content 525 * @param users The users 526 */ 527 protected void sendErrorMailToContributors(Content content, Set<UserIdentity> users) 528 { 529 try 530 { 531 List<String> params = getBodyParamsForContributors (content, false); 532 533 I18nizableText i18nSubject = new I18nizableText(null, _userErrorMailSubject, params); 534 I18nizableText i18nBody = new I18nizableText(null, _userErrorMailBody, params); 535 536 String subject = _i18nUtils.translate(i18nSubject); 537 String body = _i18nUtils.translate(i18nBody); 538 539 _sendMailsToUsers(subject, body, users, _mailFrom); 540 } 541 catch (Exception e) 542 { 543 _logger.error("Unable to send mail to contributors after archiving content '" + content.getName() + "' failed", e); 544 } 545 } 546 547 /** 548 * Get the request URI to set in mail 549 * @param content The content. Can be null 550 * @return the request URI 551 */ 552 protected String _getRequestURI (Content content) 553 { 554 return _baseUrl; 555 } 556 557 /** 558 * Send the emails to users (contributors) 559 * @param subject the e-mail subject. 560 * @param body the e-mail body. 561 * @param users users to send the mail to. 562 * @param from the address sending the e-mail. 563 */ 564 protected void _sendMailsToUsers(String subject, String body, Set<UserIdentity> users, String from) 565 { 566 for (UserIdentity userIdentity : users) 567 { 568 User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin()); 569 570 if (user != null && StringUtils.isNotBlank(user.getEmail())) 571 { 572 String mail = user.getEmail(); 573 574 try 575 { 576 SendMailHelper.newMail() 577 .withSubject(subject) 578 .withTextBody(body) 579 .withSender(from) 580 .withRecipient(mail) 581 .sendMail(); 582 } 583 catch (MessagingException | IOException e) 584 { 585 if (_logger.isWarnEnabled()) 586 { 587 _logger.warn("Could not send an archive notification e-mail to " + mail, e); 588 } 589 } 590 } 591 } 592 } 593}