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.web.alerts;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Calendar;
021import java.util.GregorianCalendar;
022import java.util.HashSet;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.configuration.ConfigurationException;
030import org.apache.cocoon.components.ContextHelper;
031import org.apache.cocoon.environment.Request;
032import org.quartz.JobDataMap;
033import org.quartz.JobExecutionContext;
034
035import org.ametys.cms.repository.Content;
036import org.ametys.core.schedule.progression.ContainerProgressionTracker;
037import org.ametys.core.ui.mail.StandardMailBodyHelper;
038import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder;
039import org.ametys.core.user.UserIdentity;
040import org.ametys.core.user.population.PopulationContextHelper;
041import org.ametys.core.util.HttpUtils;
042import org.ametys.core.util.URIUtils;
043import org.ametys.plugins.core.schedule.Scheduler;
044import org.ametys.plugins.repository.AmetysObjectIterable;
045import org.ametys.plugins.repository.AmetysRepositoryException;
046import org.ametys.plugins.repository.query.expression.DateExpression;
047import org.ametys.plugins.repository.query.expression.Expression;
048import org.ametys.plugins.repository.query.expression.Expression.Operator;
049import org.ametys.runtime.config.Config;
050import org.ametys.runtime.i18n.I18nizableText;
051import org.ametys.web.repository.content.WebContent;
052import org.ametys.web.repository.page.Page;
053import org.ametys.web.repository.page.PageQueryHelper;
054import org.ametys.web.repository.page.jcr.DefaultPage;
055import org.ametys.web.repository.site.Site;
056
057/**
058 * Alerts engine: sends alerts mail.
059 * This is the web version of the engine: it sets the currently processed content's
060 * site name in the request object, and sends additional site and page information
061 * in the alerts e-mails.
062 */
063public class AlertSchedulable extends org.ametys.cms.alerts.AlertSchedulable
064{
065    /** True if the alert of page publication ending is enabled */
066    protected boolean _pagePublicationEnabled;
067    
068    /** The "page nearing end of publication" e-mail will be sent to users that have this right. */
069    protected Set<String> _pagePublicationEndRights;
070    
071    /** The "page nearing end of publication" e-mail subject i18n key. */
072    protected String _pagePublicationEndSubject;
073    
074    /** The "page nearing end of publication" e-mail body i18n key. */
075    protected String _pagePublicationEndBody;
076    
077    /** The server base URL. */
078    protected String _baseUrl;
079    
080    @Override
081    public void initialize() throws Exception
082    {
083        super.initialize();
084        _baseUrl = HttpUtils.sanitize(Config.getInstance().getValue("cms.url"));
085    }
086    
087    @Override
088    public void configure(Configuration configuration) throws ConfigurationException
089    {
090        super.configure(configuration);
091        
092        Configuration pagePublicationEndConf = configuration.getChild("pagePublicationEnd", false);
093        
094        if (pagePublicationEndConf != null)
095        {
096            _pagePublicationEnabled = true;
097            _pagePublicationEndRights = _getRightsFromConf(pagePublicationEndConf);
098            _pagePublicationEndSubject = pagePublicationEndConf.getChild("subjectKey").getValue();
099            _pagePublicationEndBody = pagePublicationEndConf.getChild("bodyKey").getValue();
100        }
101    }
102    
103    @Override
104    public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
105    {
106        // Send the content alerts.
107        super.execute(context, progressionTracker);
108        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
109        boolean instantMode = jobDataMap.getBooleanValue(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_INSTANT_MODE_KEY);
110        if (!instantMode && _pagePublicationEnabled)
111        {
112            // Send the page alerts.
113            _sendPagePublicationEndAlerts();
114        }
115    }
116    
117    @Override
118    protected void _setRequestAttributes (Content content)
119    {
120        Request request = ContextHelper.getRequest(_context);
121        
122        Site site = _getSite(content);
123        if (site != null)
124        {
125            String siteName = site.getName();
126            // Set the site name into the request for the other components to be able to retrieve it.
127            request.setAttribute("siteName", siteName);
128            
129            List<String> populationContexts = new ArrayList<>();
130            
131            // Set the population contexts to be able to get allowed users
132            populationContexts.add("/sites/" + siteName);
133            populationContexts.add("/sites-fo/" + siteName);
134            
135            request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts);
136        }
137    }
138    
139    /**
140     * Set the necessary request attributes
141     * @param page The concerned page
142     */
143    protected void _setRequestAttributes (Page page)
144    {
145        Request request = ContextHelper.getRequest(_context);
146        
147        String siteName = page.getSiteName();
148        
149        // Set the site name into the request for the other components to be able to retrieve it.
150        request.setAttribute("siteName", siteName);
151        
152        List<String> populationContexts = new ArrayList<>();
153        
154        // Set the population contexts to be able to get allowed users
155        populationContexts.add("/sites/" + siteName);
156        populationContexts.add("/sites-fo/" + siteName);
157        
158        request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts);
159    }
160    
161    /**
162     * Send the "page nearing end of publication" alerts.
163     * @throws AmetysRepositoryException if an error occurs.
164     */
165    protected void _sendPagePublicationEndAlerts() throws AmetysRepositoryException
166    {
167        Long delay = Config.getInstance().getValue("remind.before.publication.end");
168        if (delay != null && delay > 0)
169        {
170            Calendar calendar = new GregorianCalendar();
171            _removeTimeParts(calendar);
172            calendar.add(Calendar.DAY_OF_MONTH, delay.intValue());
173            
174            // Get all the pages which publication end is in X days.
175            Expression nearPublicationEndExpr = new DateExpression(DefaultPage.METADATA_PUBLICATION_END_DATE, Operator.EQ, calendar.getTime());
176            
177            String query = PageQueryHelper.getPageXPathQuery(null, null, null, nearPublicationEndExpr, null);
178            
179            try (AmetysObjectIterable<Page> pages = _resolver.query(query))
180            {
181                if (getLogger().isInfoEnabled())
182                {
183                    getLogger().info("Pages within " + Long.toString(delay) + " days of publication end: " + pages.getSize());
184                }
185                
186                for (Page page : pages)
187                {
188                    // Send the alert e-mails.
189                    _sendPagePublicationEndEmail(page);
190                }
191            }
192        }
193    }
194    
195    /**
196     * Send the "page publication end" alert e-mail.
197     * @param page the page nearing publication end.
198     * @throws AmetysRepositoryException if an error occurs.
199     */
200    protected void _sendPagePublicationEndEmail(Page page) throws AmetysRepositoryException
201    {
202        _setRequestAttributes(page);
203        
204        Set<UserIdentity> users = new HashSet<>();
205        for (String right : _pagePublicationEndRights)
206        {
207            users.addAll(_rightManager.getAllowedUsers(right, page).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
208        }
209        
210        List<String> params = _getPagePublicationEndParams(page);
211        
212        I18nizableText i18nSubject = new I18nizableText(null, _pagePublicationEndSubject, params);
213        I18nizableText i18nBody = new I18nizableText(null, _pagePublicationEndBody, params);
214        
215        try
216        {
217            MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody()
218                    .withTitle(i18nSubject)
219                    .withMessage(i18nBody)
220                    .withLink(_getPageUrl(page), new I18nizableText("plugin.web", "CONTENT_ALERTS_MAIL_PAGE_LINK_TITLE"));
221            
222            _sendMails(i18nSubject, bodyBuilder, users, _mailFrom);
223        }
224        catch (IOException e)
225        {
226            getLogger().error("Fail to build HTML mail body for page publication end", e);
227        }
228    }
229    
230    @Override
231    protected String getI18nKeyBody(String bodyI18nKey, Content content)
232    {
233        String i18nKey = bodyI18nKey;
234        
235        Site site = _getSite(content);
236        if (site == null)
237        {
238            i18nKey += "_NOSITE";
239        }
240        else if (_getPage(content, site) == null)
241        {
242            // Orphan content
243            i18nKey += "_ORPHAN";
244        }
245        
246        return i18nKey;
247    }
248    
249    @Override
250    protected List<String> _getAdditionalParams(Content content)
251    {
252        List<String> params = new ArrayList<>();
253        
254        Site site = _getSite(content);
255        if (site != null)
256        {
257            params.add(site.getTitle());  // {4}
258        }
259        
260        Page page = _getPage(content, site);
261        if (page != null)
262        {
263            params.add(page.getTitle()); // {5}
264        }
265        
266        return params;
267    }
268    
269    /**
270     * Get the "page nearing end of publication" mail parameters.
271     * @param page the page.
272     * @return the mail parameters.
273     */
274    protected List<String> _getPagePublicationEndParams(Page page)
275    {
276        List<String> params = new ArrayList<>();
277        
278        String siteTitle = page.getSite().getTitle();
279        Long delay = Config.getInstance().getValue("remind.before.publication.end");
280        
281        params.add(page.getTitle());
282        params.add(_getPageUrl(page));
283        params.add(String.valueOf(delay));
284        params.add(siteTitle);
285        
286        return params;
287    }
288    
289    @Override
290    protected String _getContentUrl(Content content)
291    {
292        Site site = _getSite(content);
293        if (site != null)
294        {
295            Page page = _getPage(content, site);
296            if (page != null)
297            {
298                StringBuilder url = new StringBuilder(_baseUrl);
299                if (!_baseUrl.endsWith("/"))
300                {
301                    url.append('/');
302                }
303                url.append(site.getName()).append("/index.html?uitool=uitool-page,id:%27").append(URIUtils.encodeParameter(page.getId())).append("%27");
304                return url.toString();
305            }
306            else
307            {
308                return _contentHelper.getContentBOUrl(content, Map.of("siteName", site.getName()));
309            }
310        }
311        else
312        {
313            return super._getContentUrl(content);
314        }
315    }
316    
317    /**
318     * Get the URL of the back-office, opening on the page tool.
319     * @param page the page to link to.
320     * @return the page URL.
321     */
322    protected String _getPageUrl(Page page)
323    {
324        StringBuilder url = new StringBuilder(_baseUrl);
325        if (!_baseUrl.endsWith("/"))
326        {
327            url.append('/');
328        }
329        url.append(page.getSite().getName()).append("/index.html?uitool=uitool-page,id:%27").append(page.getId()).append("%27");
330        return url.toString();
331    }
332    
333    /**
334     * Get the site name
335     * @param content The content
336     * @return the site name
337     */
338    protected Site _getSite(Content content)
339    {
340        if (content instanceof WebContent)
341        {
342            return ((WebContent) content).getSite();
343        }
344        
345        return null;
346    }
347
348    /**
349     * Get the page referenced by this content
350     * @param content The content
351     * @param site the site
352     * @return the page or null.
353     */
354    protected Page _getPage(Content content, Site site)
355    {
356        if (content instanceof WebContent)
357        {
358            Iterator<Page> pages = ((WebContent) content).getReferencingPages().iterator();
359            if (pages.hasNext())
360            {
361                return pages.next();
362            }
363        }
364        return null;
365    }
366    
367}