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