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