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