001/*
002 *  Copyright 2010 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.content.consistency;
017
018import java.io.ByteArrayInputStream;
019import java.io.ByteArrayOutputStream;
020import java.io.File;
021import java.io.IOException;
022import java.io.InputStream;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import org.apache.avalon.framework.activity.Initializable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.ProcessingException;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.commons.io.IOUtils;
036import org.apache.commons.lang3.StringUtils;
037import org.apache.excalibur.source.Source;
038import org.apache.excalibur.source.SourceResolver;
039import org.quartz.JobExecutionContext;
040
041import org.ametys.cms.content.consistency.ContentConsistencyManager.ConsistenciesReport;
042import org.ametys.core.right.RightManager;
043import org.ametys.core.schedule.progression.ContainerProgressionTracker;
044import org.ametys.core.schedule.progression.SimpleProgressionTracker;
045import org.ametys.core.ui.mail.StandardMailBodyHelper;
046import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder;
047import org.ametys.core.user.User;
048import org.ametys.core.user.UserIdentity;
049import org.ametys.core.user.UserManager;
050import org.ametys.core.util.HttpUtils;
051import org.ametys.core.util.I18nUtils;
052import org.ametys.core.util.mail.SendMailHelper;
053import org.ametys.core.util.mail.SendMailHelper.NamedStream;
054import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable;
055import org.ametys.plugins.repository.AmetysObjectResolver;
056import org.ametys.runtime.config.Config;
057import org.ametys.runtime.i18n.I18nizableText;
058import org.ametys.runtime.servlet.RuntimeConfig;
059
060import jakarta.mail.MessagingException;
061
062/**
063 * Content consistency schedulable: generate consistency information for all contents.
064 * Sends a report e-mail if there are inconsistencies.
065 */
066public class CheckContentConsistencySchedulable extends AbstractStaticSchedulable implements Initializable
067{
068    /** Id of progression tracker for  removing outdated results*/
069    public static final String REMOVE_OUTDATED_RESULT = "remove-outdated-result";
070    /** Id of progression tracker for  removing outdated results*/
071    public static final String CHECK_CONTENTS = "check-contents";
072    /** The report e-mail will be sent to users who possess this right on the application context. */
073    protected static final String __MAIL_RIGHT = "CMS_Rights_ReceiveConsistencyReport";
074    /** Id of progression tracker for notifications */
075    protected static final String __CHECK_CONTENT_CONSISTENCY_NOTIFICATION = "check-content-consistency-notification";
076    /** Id of progression tracker for consistensy report */
077    protected static final String __CHECK_CONTENT_CONSISTENCY_REPORT = "check-content-consistency-report";
078    
079    /** The server base URL. */
080    protected String _baseUrl;
081    
082    /** The report directory. */
083    protected File _reportDirectory;
084    
085    /** The ametys object resolver. */
086    protected AmetysObjectResolver _ametysResolver;
087    
088    /** The avalon source resolver. */
089    protected SourceResolver _sourceResolver;
090    
091    /** The rights manager. */
092    protected RightManager _rightManager;
093    
094    /** The i18n utils. */
095    protected I18nUtils _i18nUtils;
096    
097    /** The content of "from" field in emails. */
098    protected String _mailFrom;
099    
100    /** The content consistency manager */
101    protected ContentConsistencyManager _contentConsistencyManager;
102
103    private ContentConsistencySearchModel _contentConsistencySearchModel;
104    
105    @Override
106    public void service(ServiceManager manager) throws ServiceException
107    {
108        super.service(manager);
109        
110        _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
111        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
112        
113        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
114        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
115        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
116        
117        _contentConsistencySearchModel = (ContentConsistencySearchModel) manager.lookup(ContentConsistencySearchModel.ROLE);
118        _contentConsistencyManager = (ContentConsistencyManager) manager.lookup(ContentConsistencyManager.ROLE);
119    }
120    
121    public void initialize() throws Exception
122    {
123        _baseUrl = HttpUtils.sanitize(Config.getInstance().getValue("cms.url"));
124        _mailFrom = Config.getInstance().getValue("smtp.mail.from");
125        _reportDirectory = new File(RuntimeConfig.getInstance().getAmetysHome(), "consistency");
126    }
127    
128    @Override
129    public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
130    {
131        // Step's weight has been arbitrarily chosen because creating report is way longer than sending emails
132        ContainerProgressionTracker reportStep = progressionTracker.addContainerStep(__CHECK_CONTENT_CONSISTENCY_REPORT, new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_REPORT_STEP_LABEL"), 10);
133        reportStep.addSimpleStep(CHECK_CONTENTS, new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_CHECK_CONTENTS_LABEL"));
134        reportStep.addSimpleStep(REMOVE_OUTDATED_RESULT, new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_REMOVE_OUTDATED_RESULT_LABEL"));
135        SimpleProgressionTracker sendEmailStep = progressionTracker.addSimpleStep(__CHECK_CONTENT_CONSISTENCY_NOTIFICATION, new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_NOTIFICATION_STEPLABEL"));
136     
137        ConsistenciesReport report = _contentConsistencyManager.checkAllContents(reportStep);
138        
139        Set<UserIdentity> users = getUsersToNotify();
140        if (!report.isEmpty() && !users.isEmpty())
141        {
142            sendEmailStep.setSize(users.size());
143            _sendEmail(report, users, sendEmailStep);
144        }
145    }
146
147    /**
148     * Send a reminder e-mail to all the users who have the right to edit.
149     * @param report the consistency report
150     * @param users the users to notify
151     * @param progressionTracker the progression tracker for sending email
152     * @throws IOException if an error occurs building or sending the mail.
153     * @throws ProcessingException if an error occurs
154     */
155    protected void _sendEmail(ConsistenciesReport report, Set<UserIdentity> users, SimpleProgressionTracker progressionTracker) throws IOException, ProcessingException
156    {
157        I18nizableText i18nSubject = _getMailSubject(report);
158        String subject = _i18nUtils.translate(i18nSubject);
159        
160        String htmlBody = _getMailBody(report);
161        
162        if (StringUtils.isEmpty(htmlBody))
163        {
164            return;
165        }
166        try (InputStream attachment = _getReport(report.results()))
167        {
168            _sendMails(subject, htmlBody, users, _mailFrom, attachment, progressionTracker);
169        }
170    }
171
172    /**
173     * Compute the list of user to notify
174     * @return a list of user identity
175     */
176    protected Set<UserIdentity> getUsersToNotify()
177    {
178        Set<UserIdentity> users = _rightManager.getAllowedUsers(__MAIL_RIGHT, "/cms").resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"));
179        return users;
180    }
181    
182    private InputStream _getReport(List<String> resultsId) throws ProcessingException
183    {
184        Request request = ContextHelper.getRequest(_context);
185        Source attachement = null;
186        try
187        {
188            
189            List<Map<String, Object>> results = new ArrayList<>();
190            for (String resultId : resultsId)
191            {
192                ContentConsistencyResult result = _ametysResolver.resolveById(resultId);
193                List<Map<String, Object>> columns = _contentConsistencySearchModel.getColumns();
194                results.add(_contentConsistencyManager.resultToJSON(result, columns));
195            }
196            request.setAttribute(ContentConsistencyResultGenerator.RESULTS_REQUEST_ATTRIBUTE_NAME, results);
197            
198            attachement = _sourceResolver.resolveURI("cocoon://_plugins/cms/consistency/report.csv");
199            InputStream attachmentCopy = null;
200            // Get the attachment and store it in a reusable inputStream.
201            // If the attachment size exceed 1M we won't attach it
202            try (InputStream is = attachement.getInputStream())
203            {
204                // Override the byteArray to avoid duplication of buffer during copy
205                ByteArrayOutputStream tmp = new ByteArrayOutputStream()
206                {
207                    @Override
208                    public synchronized byte[] toByteArray()
209                    {
210                        return buf;
211                    }
212                };
213                IOUtils.copyLarge(is, tmp, 0, 1_000_000);
214                if (is.read() == -1)
215                {
216                    // We could read all the stream so the size is acceptable and we keep the attachment
217                    attachmentCopy = new ByteArrayInputStream(tmp.toByteArray());
218                }
219                
220            }
221            catch (IOException e)
222            {
223                getLogger().error("Failed to retrieve consistency report", e);
224            }
225            return attachmentCopy;
226        }
227        catch (IOException e)
228        {
229            getLogger().error("Failed to retrieve report", e);
230            return null;
231        }
232        finally
233        {
234            _sourceResolver.release(attachement);
235            request.removeAttribute(ContentConsistencyResultGenerator.RESULTS_REQUEST_ATTRIBUTE_NAME);
236        }
237    }
238
239    /**
240     * Retrieves the mail's subject
241     * @param report the consistency report
242     * @return the mail's subject
243     */
244    protected I18nizableText _getMailSubject(ConsistenciesReport report)
245    {
246        return new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_SUBJECT");
247    }
248    
249    /**
250     * Retrieves the mail's body
251     * @param report the consistency report
252     * @return the mail's body
253     */
254    protected String _getMailBody(ConsistenciesReport report)
255    {
256        try
257        {
258            MailBodyBuilder mailBuilder = StandardMailBodyHelper.newHTMLBody()
259                    .withTitle(_getMailSubject(report));
260                
261            int nbFailure = report.results() != null ? report.results().size() : 0;
262            
263            if (nbFailure == 1)
264            {
265                mailBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_FAILURE"));
266            }
267            else if (nbFailure > 1)
268            {
269                mailBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_FAILURES", List.of(String.valueOf(nbFailure))));
270            }
271            
272            if (!(report.unchecked() == null || report.unchecked().isEmpty()))
273            {
274                mailBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_UNCHECKED", List.of(StringUtils.join(report.unchecked(), ", "))));
275            }
276            
277            mailBuilder.addMessage(new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_REPORT", List.of(_getReportUri())));
278            
279            mailBuilder.withLink(_getReportUri(), new I18nizableText("plugin.cms", "PLUGINS_CMS_GLOBAL_CONTENT_CONSISTENCY_MAIL_BODY_REPORT_LINK_TITLE"));
280
281            return mailBuilder.build();
282        }
283        catch (IOException e)
284        {
285            getLogger().error("Fail to build HTML content consistency mail report", e);
286            return null;
287        }
288    }
289    
290    /**
291     * Get the report URI
292     * @return the report uri
293     */
294    protected String _getReportUri()
295    {
296        StringBuilder url = new StringBuilder(_baseUrl);
297        url.append("/index.html?uitool=uitool-global-consistency");
298        return url.toString();
299    }
300    
301    /**
302     * Send the alert e-mails.
303     * @param subject the e-mail subject.
304     * @param body the e-mail body.
305     * @param users users to send the mail to.
306     * @param from the address sending the e-mail.
307     * @param attachment the e-mail attachment
308     * @param progressionTracker progression tracker for sending report mail
309     */
310    protected void _sendMails(String subject, String body, Set<UserIdentity> users, String from, InputStream attachment, SimpleProgressionTracker progressionTracker)
311    {
312        for (UserIdentity userIdentity : users)
313        {
314            User user = _userManager.getUser(userIdentity);
315            
316            if (user != null && StringUtils.isNotBlank(user.getEmail()))
317            {
318                String mail = user.getEmail();
319                
320                Collection<NamedStream> attachments = null;
321                if (attachment != null)
322                {
323                    try
324                    {
325                        attachment.reset();
326                    }
327                    catch (IOException e)
328                    {
329                        // in case reset is not implemented.
330                    }
331                    attachments = List.of(new NamedStream(attachment, "report.csv", "text/csv"));
332                }
333                
334                try
335                {
336                    SendMailHelper.newMail()
337                                  .withSubject(subject)
338                                  .withHTMLBody(body)
339                                  .withSender(from)
340                                  .withRecipient(mail)
341                                  .withAttachmentsAsStream(attachments)
342                                  .sendMail();
343                }
344                catch (MessagingException | IOException e)
345                {
346                    if (getLogger().isWarnEnabled())
347                    {
348                        getLogger().warn("Could not send an alert e-mail to " + mail, e);
349                    }
350                }
351            }
352            progressionTracker.increment();
353        }
354    }
355}