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