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