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