001/* 002 * Copyright 2021 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.purge; 017 018import java.io.ByteArrayOutputStream; 019import java.io.IOException; 020import java.io.InputStream; 021import java.time.ZoneId; 022import java.util.Date; 023import java.util.HashMap; 024import java.util.Map; 025 026import org.apache.avalon.framework.activity.Initializable; 027import org.apache.avalon.framework.configuration.Configuration; 028import org.apache.avalon.framework.configuration.ConfigurationException; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.cocoon.components.ContextHelper; 032import org.apache.cocoon.environment.Request; 033import org.apache.commons.lang.StringUtils; 034import org.apache.commons.lang.exception.ExceptionUtils; 035import org.apache.excalibur.source.Source; 036import org.apache.excalibur.source.SourceResolver; 037import org.apache.excalibur.source.SourceUtil; 038import org.quartz.JobExecutionContext; 039 040import org.ametys.cms.repository.Content; 041import org.ametys.cms.repository.ContentQueryHelper; 042import org.ametys.cms.repository.WorkflowAwareContent; 043import org.ametys.core.schedule.progression.ContainerProgressionTracker; 044import org.ametys.core.ui.mail.StandardMailBodyHelper; 045import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder; 046import org.ametys.core.util.DateUtils; 047import org.ametys.core.util.HttpUtils; 048import org.ametys.core.util.I18nUtils; 049import org.ametys.core.util.mail.SendMailHelper; 050import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable; 051import org.ametys.plugins.repository.AmetysObjectIterable; 052import org.ametys.plugins.repository.AmetysObjectResolver; 053import org.ametys.plugins.workflow.support.WorkflowProvider; 054import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 055import org.ametys.runtime.config.Config; 056import org.ametys.runtime.i18n.I18nizableText; 057 058import jakarta.mail.MessagingException; 059 060/** 061 * Schedulable that removes old versions of contents. 062 */ 063public class PurgeContentsSchedulable extends AbstractStaticSchedulable implements Initializable 064{ 065 /** The server base URL. */ 066 protected String _baseUrl; 067 068 /** The ametys object resolver. */ 069 protected AmetysObjectResolver _ametysResolver; 070 071 /** The avalon source resolver. */ 072 protected SourceResolver _sourceResolver; 073 074 /** The version purger. */ 075 protected PurgeVersionsManager _versionPurger; 076 077 /** The workflow provider */ 078 protected WorkflowProvider _workflowProvider; 079 080 /** The i18n utils. */ 081 protected I18nUtils _i18nUtils; 082 083 /** A Map of the validation step ID by workflow name. */ 084 protected Map<String, Long> _validationStepId; 085 086 /** The count of oldest versions to keep. */ 087 protected int _firstVersionsToKeep; 088 089 /** The content of "from" field in emails. */ 090 protected String _mailFrom; 091 092 /** The sysadmin mail address, to which will be sent the report e-mail. */ 093 protected String _sysadminMail; 094 095 @Override 096 public void service(ServiceManager manager) throws ServiceException 097 { 098 super.service(manager); 099 // Lookup the needed components. 100 _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 101 _versionPurger = (PurgeVersionsManager) manager.lookup(PurgeVersionsManager.ROLE); 102 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 103 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 104 105 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 106 } 107 108 public void initialize() throws Exception 109 { 110 _baseUrl = HttpUtils.sanitize(Config.getInstance().getValue("cms.url")); 111 _mailFrom = Config.getInstance().getValue("smtp.mail.from"); 112 _sysadminMail = Config.getInstance().getValue("smtp.mail.sysadminto"); 113 } 114 115 @Override 116 public void configure(Configuration configuration) throws ConfigurationException 117 { 118 super.configure(configuration); 119 _validationStepId = configureValidationStepId(configuration); 120 _firstVersionsToKeep = configuration.getChild("firstVersionsToKeep").getValueAsInteger(); 121 } 122 123 /** 124 * Get the validation step ID by workflow from the component configuration. 125 * @param configuration the component configuration. 126 * @return a Map of the validation step ID by workflow. 127 * @throws ConfigurationException If an error occurred 128 */ 129 protected Map<String, Long> configureValidationStepId(Configuration configuration) throws ConfigurationException 130 { 131 Map<String, Long> validationStepIds = new HashMap<>(); 132 133 for (Configuration workflowConf : configuration.getChildren("workflow")) 134 { 135 String workflowName = workflowConf.getAttribute("name"); 136 Long validationStepId = workflowConf.getChild("validationStepId").getValueAsLong(); 137 138 validationStepIds.put(workflowName, validationStepId); 139 } 140 141 return validationStepIds; 142 } 143 144 @Override 145 public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception 146 { 147 String query = ContentQueryHelper.getContentXPathQuery(null); 148 149 Date startDate = new Date(); 150 151 int totalContentsPurged = 0; 152 int totalVersionsPurged = 0; 153 154 try 155 { 156 try (AmetysObjectIterable<Content> contents = _ametysResolver.query(query)) 157 { 158 for (Content content : contents) 159 { 160 if (content instanceof WorkflowAwareContent) 161 { 162 WorkflowAwareContent workflowContent = (WorkflowAwareContent) content; 163 long workflowId = workflowContent.getWorkflowId(); 164 165 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(workflowContent); 166 String workflowName = workflow.getWorkflowName(workflowId); 167 168 if (_validationStepId.containsKey(workflowName)) 169 { 170 long validationStepId = _validationStepId.get(workflowName); 171 172 int purged = _versionPurger.purgeContent(workflowContent, validationStepId, _firstVersionsToKeep); 173 if (purged > 0) 174 { 175 totalVersionsPurged += purged; 176 totalContentsPurged++; 177 } 178 } 179 } 180 } 181 } 182 183 Date endDate = new Date(); 184 185 sendMail(startDate, endDate, totalContentsPurged, totalVersionsPurged); 186 } 187 catch (Exception e) 188 { 189 sendErrorMail(startDate, e); 190 } 191 192 } 193 194 /** 195 * Send the purge report e-mail. 196 * @param startDate the purge start date. 197 * @param endDate the purge end date. 198 * @param totalContentsPurged the total count of contents of which versions were purged. 199 * @param totalVersionsPurged the total count of content versions removed. 200 */ 201 protected void sendMail(Date startDate, Date endDate, int totalContentsPurged, int totalVersionsPurged) 202 { 203 try 204 { 205 String language = _userLanguagesManager.getDefaultLanguage(); 206 Map<String, String> params = getEmailParams(startDate, endDate, totalContentsPurged, totalVersionsPurged); 207 208 I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_PURGE_CONTENTS_REPORT_SUBJECT"); 209 210 String subject = _i18nUtils.translate(i18nSubject, language); 211 String body = getMailBody(params, language); 212 213 if (StringUtils.isNotBlank(_sysadminMail)) 214 { 215 SendMailHelper.newMail() 216 .withSubject(subject) 217 .withHTMLBody(body) 218 .withSender(_mailFrom) 219 .withRecipient(_sysadminMail) 220 .sendMail(); 221 } 222 } 223 catch (MessagingException | IOException e) 224 { 225 getLogger().warn("Error sending the purge report e-mail.", e); 226 } 227 } 228 229 /** 230 * Send the error e-mail. 231 * @param startDate the purge start date. 232 * @param throwable the error. 233 */ 234 protected void sendErrorMail(Date startDate, Throwable throwable) 235 { 236 try 237 { 238 String language = _userLanguagesManager.getDefaultLanguage(); 239 Map<String, String> params = getErrorEmailParams(startDate, throwable); 240 241 I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_PURGE_CONTENTS_REPORT_ERROR_SUBJECT"); 242 243 String subject = _i18nUtils.translate(i18nSubject, language); 244 String mailUri = getErrorMailUri(params); 245 String body = getMailBody(mailUri, params, language, throwable); 246 247 if (StringUtils.isNotBlank(_sysadminMail)) 248 { 249 SendMailHelper.newMail() 250 .withSubject(subject) 251 .withHTMLBody(body) 252 .withSender(_mailFrom) 253 .withRecipient(_sysadminMail) 254 .sendMail(); 255 } 256 } 257 catch (MessagingException | IOException e) 258 { 259 getLogger().warn("Error sending the purge error e-mail.", e); 260 } 261 } 262 263 /** 264 * Get the report e-mail parameters. 265 * @param startDate the purge start date. 266 * @param endDate the purge end date. 267 * @param totalContentsPurged the total count of contents of which versions were purged. 268 * @param totalVersionsPurged the total count of content versions removed. 269 * @return the e-mail parameters. 270 */ 271 protected Map<String, String> getEmailParams(Date startDate, Date endDate, int totalContentsPurged, int totalVersionsPurged) 272 { 273 Map<String, String> params = new HashMap<>(); 274 275 params.put("startDate", DateUtils.getISODateTimeFormatter().format(startDate.toInstant().atZone(ZoneId.systemDefault()))); 276 params.put("endDate", DateUtils.getISODateTimeFormatter().format(endDate.toInstant().atZone(ZoneId.systemDefault()))); 277 params.put("totalContentsPurged", Integer.toString(totalContentsPurged)); 278 params.put("totalVersionsPurged", Integer.toString(totalVersionsPurged)); 279 params.put("url", _baseUrl); 280 281 return params; 282 } 283 284 /** 285 * Get the error e-mail parameters. 286 * @param startDate the purge start date. 287 * @param throwable the error. 288 * @return the e-mail parameters. 289 */ 290 protected Map<String, String> getErrorEmailParams(Date startDate, Throwable throwable) 291 { 292 Map<String, String> params = new HashMap<>(); 293 294 params.put("startDate", DateUtils.getISODateTimeFormatter().format(startDate.toInstant().atZone(ZoneId.systemDefault()))); 295 params.put("url", _baseUrl); 296 297 return params; 298 } 299 300 /** 301 * Get a mail part. 302 * @param parameters the pipeline parameters. 303 * @param language The language to use for the mail. 304 * @return the mail part. 305 * @throws IOException If an error occurred 306 */ 307 protected String getMailBody(Map<String, String> parameters, String language) throws IOException 308 { 309 String uri = getMailUri(parameters); 310 311 return getMailBody(uri, parameters, language, null); 312 } 313 314 /** 315 * Get a mail part. 316 * @param uri The url where to get the body of the mail 317 * @param parameters the pipeline parameters. 318 * @return the mail part. 319 * @throws IOException If an error occurred 320 */ 321 private String getMailBody(String uri, Map<String, String> parameters, String language, Throwable throwable) throws IOException 322 { 323 Request request = ContextHelper.getRequest(_context); 324 Object previousLang = request.getAttribute("lang"); 325 326 Source source = null; 327 InputStream is = null; 328 try 329 { 330 request.setAttribute("lang", language); 331 332 source = _sourceResolver.resolveURI(uri, null, parameters); 333 is = source.getInputStream(); 334 335 try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) 336 { 337 SourceUtil.copy(is, bos); 338 339 String message = bos.toString("UTF-8"); 340 MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody() 341 .withTitle(new I18nizableText("plugin.cms", throwable != null ? "PLUGINS_CMS_PURGE_CONTENTS_REPORT_ERROR_SUBJECT" : "PLUGINS_CMS_PURGE_CONTENTS_REPORT_SUBJECT")) 342 .withMessage(message) 343 .withLink(_baseUrl, new I18nizableText("plugin.cms", "PLUGINS_CMS_PURGE_CONTENTS_REPORT_BODY_LINK")) 344 .withLanguage(language); 345 346 if (throwable != null) 347 { 348 String stackTrace = ExceptionUtils.getStackTrace(throwable); 349 bodyBuilder.withDetails(new I18nizableText("plugin.cms", "PLUGINS_CMS_PURGE_CONTENTS_REPORT_BODY_ERROR_TITLE"), stackTrace, true); 350 } 351 352 return bodyBuilder.build(); 353 } 354 } 355 finally 356 { 357 request.setAttribute("lang", previousLang); 358 359 if (is != null) 360 { 361 is.close(); 362 } 363 364 if (source != null) 365 { 366 _sourceResolver.release(source); 367 } 368 } 369 } 370 371 /** 372 * Get the pipeline uri for mail body 373 * @param parameters the mail parameters 374 * @return a pipeline uri 375 */ 376 protected String getMailUri(Map<String, String> parameters) 377 { 378 return "cocoon://_plugins/cms/purge/purge-mail.html"; 379 } 380 381 /** 382 * Get the pipeline uri for error mail body. 383 * @param parameters the mail parameters 384 * @return a pipeline uri 385 */ 386 protected String getErrorMailUri(Map<String, String> parameters) 387 { 388 return "cocoon://_plugins/cms/purge/purge-error-mail.html"; 389 } 390 391}