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