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