001/* 002 * Copyright 2020 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.plugins.core.ui.script; 017 018import java.io.File; 019import java.io.IOException; 020import java.time.Duration; 021import java.time.ZoneId; 022import java.time.ZonedDateTime; 023import java.time.format.DateTimeFormatter; 024import java.time.format.FormatStyle; 025import java.util.ArrayList; 026import java.util.List; 027import java.util.Locale; 028import java.util.Map; 029import java.util.Optional; 030import java.util.function.Function; 031 032import javax.mail.MessagingException; 033 034import org.apache.avalon.framework.activity.Initializable; 035import org.apache.commons.lang3.StringUtils; 036import org.apache.commons.lang3.time.DurationFormatUtils; 037import org.slf4j.Logger; 038 039import org.ametys.core.user.UserIdentity; 040import org.ametys.core.util.DateUtils; 041import org.ametys.core.util.mail.SendMailHelper; 042import org.ametys.runtime.config.Config; 043import org.ametys.runtime.i18n.I18nizableText; 044import org.ametys.runtime.plugin.PluginsManager; 045 046/** 047 * Component able to execute scripts asynchronously, either from a schedulable or because the user asked to do so. 048 * Script results are sent by email. 049 */ 050public class AsyncScriptHandler extends ScriptHandler implements Initializable 051{ 052 /** Avalon role. */ 053 public static final String COMPONENT_ROLE = AsyncScriptHandler.class.getName(); 054 055 private static final String __ASYNC_EXECUTE_MAIL_SUBJECT_KEY = "PLUGINS_CORE_UI_SCRIPT_ASYNC_EXECUTE_MAIL_SUBJECT"; 056 private static final String __ASYNC_EXECUTE_MAIL_SUBJECT_ERROR_KEY = "PLUGINS_CORE_UI_SCRIPT_ASYNC_EXECUTE_MAIL_SUBJECT_ERROR"; 057 private static final String __ASYNC_EXECUTE_MAIL_BODY_KEY = "PLUGINS_CORE_UI_SCRIPT_ASYNC_EXECUTE_MAIL_BODY"; 058 private static final String __ASYNC_EXECUTE_MAIL_UNDEFINED = "PLUGINS_CORE_UI_SCRIPT_ASYNC_EXECUTE_MAIL_UNDEFINED"; 059 060 private Function<String, String> _mailSenderProvider; 061 062 @Override 063 public void initialize() throws Exception 064 { 065 _initializeMailSenderProvider(); 066 } 067 068 /** 069 * Initializes {@link #_mailSenderProvider}. The argument of the provider is the mail of the recipient (as string) 070 */ 071 protected void _initializeMailSenderProvider() 072 { 073 if (PluginsManager.getInstance().isSafeMode()) 074 { 075 // Safe mode, the sender will be the recipient of the mail (because of the absence of information about a potential sender in this mode) 076 _mailSenderProvider = mailRecipient -> mailRecipient; 077 } 078 else 079 { 080 String mailSender = Config.getInstance().getValue("smtp.mail.from"); 081 // Normal mode, the sender will always be the sender of the Configuration 082 _mailSenderProvider = mailRecipient -> mailSender; 083 } 084 } 085 086 @Override 087 protected ResultProcessor getProcessor() 088 { 089 return new AsyncResultProcessor(); 090 } 091 092 /** 093 * Send a mail report from the result of a script 094 * @param scriptResults The result of the script 095 * @param user The user 096 * @param mailRecipient The recipient of the result mail 097 * @param locale The user locale 098 * @param logger The logger 099 * @throws MessagingException If an error occurred while preparing or sending email 100 * @throws IOException If an error occurred with an attachment. 101 */ 102 public void sendReportMail(Map<String, Object> scriptResults, UserIdentity user, String mailRecipient, Locale locale, Logger logger) throws MessagingException, IOException 103 { 104 if (StringUtils.isNotBlank(mailRecipient)) 105 { 106 String output = (String) scriptResults.get("output"); 107 String error = StringUtils.defaultString((String) scriptResults.get("error")); 108 String errorMessage = StringUtils.defaultString((String) scriptResults.get("message")); 109 String errorStacktrace = StringUtils.defaultString((String) scriptResults.get("stacktrace")); 110 111 @SuppressWarnings("unchecked") 112 List<File> attachments = (List<File>) scriptResults.get("attachments"); 113 114 Object result = scriptResults.getOrDefault("result", ""); 115 116 String i18nCatalog = "plugin.core-ui"; 117 I18nizableText undefinedText = new I18nizableText(i18nCatalog, __ASYNC_EXECUTE_MAIL_UNDEFINED); 118 119 // Define start date and duration 120 ZonedDateTime startDate = _getZDTFromScriptResults(scriptResults, "start"); 121 ZonedDateTime endDate = startDate != null ? _getZDTFromScriptResults(scriptResults, "end") : null; 122 123 I18nizableText duration = Optional.ofNullable(endDate) 124 .map(end -> Duration.between(startDate, endDate)) 125 .map(Duration::toMillis) 126 .map(DurationFormatUtils::formatDurationHMS) 127 .map(I18nizableText::new) 128 .orElse(undefinedText); 129 130 I18nizableText start = Optional.ofNullable(startDate) 131 .map(DateTimeFormatter 132 .ofLocalizedDateTime(FormatStyle.LONG) 133 .withLocale(locale) 134 .withZone(ZoneId.systemDefault()) 135 ::format 136 ) 137 .map(I18nizableText::new) 138 .orElse(undefinedText); 139 140 String htmlBody = _i18nUtils.translate( 141 new I18nizableText( 142 i18nCatalog, 143 __ASYNC_EXECUTE_MAIL_BODY_KEY, 144 Map.of( 145 "out", new I18nizableText(output), 146 "htmlResult", new I18nizableText(ScriptResultFormatter.htmlFormatResult(result)), 147 "error", new I18nizableText(error), 148 "message", new I18nizableText(errorMessage), 149 "htmlStacktrace", new I18nizableText(ScriptResultFormatter.htmlFormatStacktrace(errorStacktrace)), 150 "start", start, 151 "duration", duration 152 ) 153 ) 154 ); 155 String title = _mailTitle(i18nCatalog, errorStacktrace); 156 157 String mailSender = _mailSenderProvider.apply(mailRecipient); 158 SendMailHelper.sendMail(title, htmlBody, null, attachments, mailRecipient, mailSender); 159 } 160 else 161 { 162 logger.warn("User {} launched an asynchronous script but there is no email to send the output.", user); 163 } 164 } 165 166 private ZonedDateTime _getZDTFromScriptResults(Map<String, Object> scriptResults, String paramName) 167 { 168 return Optional.of(paramName) 169 .map(scriptResults::get) 170 .map(String.class::cast) 171 .map(DateUtils::parseZonedDateTime) 172 .orElse(null); 173 } 174 175 private String _mailTitle(String i18nCatalog, String errorStacktrace) 176 { 177 String i18nKey = StringUtils.isEmpty(errorStacktrace) 178 ? __ASYNC_EXECUTE_MAIL_SUBJECT_KEY 179 : __ASYNC_EXECUTE_MAIL_SUBJECT_ERROR_KEY; 180 return _i18nUtils.translate(new I18nizableText(i18nCatalog, i18nKey)); 181 } 182 183 static class AsyncResultProcessor extends ResultProcessor 184 { 185 @Override 186 protected Object process(Map<String, Object> results, Object scriptResult) 187 { 188 if (scriptResult instanceof File) 189 { 190 @SuppressWarnings("unchecked") 191 List<File> attachments = (List<File>) results.computeIfAbsent("attachments", __ -> new ArrayList<>()); 192 attachments.add((File) scriptResult); 193 194 return ((File) scriptResult).getName(); 195 } 196 197 return super.process(results, scriptResult); 198 } 199 } 200}