001/* 002 Licensed to the Apache Software Foundation (ASF) under one 003 or more contributor license agreements. See the NOTICE file 004 distributed with this work for additional information 005 regarding copyright ownership. The ASF licenses this file 006 to you under the Apache License, Version 2.0 (the 007 "License"); you may not use this file except in compliance 008 with the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, 013 software distributed under the License is distributed on an 014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 KIND, either express or implied. See the License for the 016 specific language governing permissions and limitations 017 under the License. 018 */ 019package org.apache.wiki.plugin; 020 021import org.apache.logging.log4j.LogManager; 022import org.apache.logging.log4j.Logger; 023import org.apache.wiki.api.core.Context; 024import org.apache.wiki.api.core.ContextEnum; 025import org.apache.wiki.api.core.Engine; 026import org.apache.wiki.api.core.Page; 027import org.apache.wiki.api.exceptions.PluginException; 028import org.apache.wiki.api.exceptions.ProviderException; 029import org.apache.wiki.api.plugin.ParserStagePlugin; 030import org.apache.wiki.api.plugin.Plugin; 031import org.apache.wiki.api.plugin.PluginElement; 032import org.apache.wiki.api.providers.WikiProvider; 033import org.apache.wiki.auth.AuthorizationManager; 034import org.apache.wiki.auth.permissions.PagePermission; 035import org.apache.wiki.pages.PageManager; 036import org.apache.wiki.preferences.Preferences; 037import org.apache.wiki.preferences.Preferences.TimeFormat; 038import org.apache.wiki.references.ReferenceManager; 039import org.apache.wiki.render.RenderingManager; 040import org.apache.wiki.util.TextUtil; 041 042import java.text.DateFormat; 043import java.text.MessageFormat; 044import java.text.ParseException; 045import java.text.SimpleDateFormat; 046import java.util.ArrayList; 047import java.util.Calendar; 048import java.util.Comparator; 049import java.util.Date; 050import java.util.Iterator; 051import java.util.List; 052import java.util.Locale; 053import java.util.Map; 054import java.util.ResourceBundle; 055import java.util.Set; 056import java.util.regex.Matcher; 057import java.util.regex.Pattern; 058 059/** 060 * <p>Builds a simple weblog. 061 * The pageformat can use the following params:</p> 062 * <p>%p - Page name</p> 063 * <p>Parameters:</p> 064 * <ul> 065 * <li><b>page</b> - which page is used to do the blog; default is the current page.</li> 066 * <li><b>entryFormat</b> - how to display the date on pages, using the J2SE SimpleDateFormat 067 * syntax. Defaults to the current locale's DateFormat.LONG format 068 * for the date, and current locale's DateFormat.SHORT for the time. 069 * Thus, for the US locale this will print dates similar to 070 * this: September 4, 2005 11:54 PM</li> 071 * <li><b>days</b> - how many days the weblog aggregator should show. If set to 072 * "all", shows all pages.</li> 073 * <li><b>pageformat</b> - What the entry pages should look like.</li> 074 * <li><b>startDate</b> - Date when to start. Format is "ddMMyy."</li> 075 * <li><b>maxEntries</b> - How many entries to show at most.</li> 076 * <li><b>preview</b> - How many characters of the text to show on the preview page.</li> 077 * </ul> 078 * <p>The "days" and "startDate" can also be sent in HTTP parameters, 079 * and the names are "weblog.days" and "weblog.startDate", respectively.</p> 080 * <p>The weblog plugin also adds an attribute to each page it is on: 081 * "weblogplugin.isweblog" is set to "true". This can be used to quickly 082 * peruse pages which have weblogs.</p> 083 * @since 1.9.21 084 */ 085 086// FIXME: Add "entries" param as an alternative to "days". 087// FIXME: Entries arrive in wrong order. 088 089public class WeblogPlugin implements Plugin, ParserStagePlugin { 090 091 private static final Logger LOG = LogManager.getLogger(WeblogPlugin.class); 092 private static final Pattern HEADINGPATTERN; 093 094 /** How many days are considered by default. Default value is {@value} */ 095 private static final int DEFAULT_DAYS = 7; 096 private static final String DEFAULT_PAGEFORMAT = "%p_blogentry_"; 097 098 /** The default date format used in the blog entry page names. */ 099 public static final String DEFAULT_DATEFORMAT = "ddMMyy"; 100 101 /** Parameter name for the startDate. Value is <tt>{@value}</tt>. */ 102 public static final String PARAM_STARTDATE = "startDate"; 103 /** Parameter name for the entryFormat. Value is <tt>{@value}</tt>. */ 104 public static final String PARAM_ENTRYFORMAT = "entryFormat"; 105 /** Parameter name for the days. Value is <tt>{@value}</tt>. */ 106 public static final String PARAM_DAYS = "days"; 107 /** Parameter name for the allowComments. Value is <tt>{@value}</tt>. */ 108 public static final String PARAM_ALLOWCOMMENTS = "allowComments"; 109 /** Parameter name for the maxEntries. Value is <tt>{@value}</tt>. */ 110 public static final String PARAM_MAXENTRIES = "maxEntries"; 111 /** Parameter name for the page. Value is <tt>{@value}</tt>. */ 112 public static final String PARAM_PAGE = "page"; 113 /** Parameter name for the preview. Value is <tt>{@value}</tt>. */ 114 public static final String PARAM_PREVIEW = "preview"; 115 116 /** The attribute which is stashed to the WikiPage attributes to check if a page 117 * is a weblog or not. You may check for its presence. 118 */ 119 public static final String ATTR_ISWEBLOG = "weblogplugin.isweblog"; 120 121 static { 122 // This is a pretty ugly, brute-force regex. But it will do for now... 123 HEADINGPATTERN = Pattern.compile("(<h[1-4][^>]*>)(.*)(</h[1-4]>)", Pattern.CASE_INSENSITIVE); 124 } 125 126 /** 127 * Create an entry name based on the blogname, a date, and an entry number. 128 * 129 * @param pageName Name of the blog 130 * @param date The date (in ddMMyy format) 131 * @param entryNum The entry number. 132 * @return A formatted page name. 133 */ 134 public static String makeEntryPage( final String pageName, final String date, final String entryNum ) { 135 return TextUtil.replaceString(DEFAULT_PAGEFORMAT,"%p",pageName)+date+"_"+entryNum; 136 } 137 138 /** 139 * Return just the basename for entires without date and entry numebr. 140 * 141 * @param pageName The name of the blog. 142 * @return A formatted name. 143 */ 144 public static String makeEntryPage( final String pageName ) 145 { 146 return TextUtil.replaceString(DEFAULT_PAGEFORMAT,"%p",pageName); 147 } 148 149 /** 150 * Returns the entry page without the entry number. 151 * 152 * @param pageName Blog name. 153 * @param date The date. 154 * @return A base name for the blog entries. 155 */ 156 public static String makeEntryPage( final String pageName, final String date ) { 157 return TextUtil.replaceString(DEFAULT_PAGEFORMAT,"%p",pageName)+date; 158 } 159 160 @Override 161 public String getDisplayName(Locale locale) { 162 final ResourceBundle rb = ResourceBundle.getBundle(PluginManager.PLUGIN_I18N_RESOURCE, locale); 163 return rb.getString(this.getClass().getSimpleName()); 164 } 165 166 @Override 167 public String getSnipExample() { 168 final SimpleDateFormat fmt = new SimpleDateFormat( DEFAULT_DATEFORMAT ); 169 String date = fmt.format(new Date(System.currentTimeMillis()-(30*24*60*60*1000L))); 170 return "WeblogPlugin page='{pagename}' startDate='" + date + "' days='30' maxEntries='30' allowComments='false'"; 171 } 172 173 174 /** 175 * {@inheritDoc} 176 */ 177 @Override 178 public String execute( final Context context, final Map< String, String > params ) throws PluginException { 179 final Calendar startTime; 180 final Calendar stopTime; 181 int numDays = DEFAULT_DAYS; 182 final Engine engine = context.getEngine(); 183 final AuthorizationManager mgr = engine.getManager( AuthorizationManager.class ); 184 185 // 186 // Parse parameters. 187 // 188 String days; 189 final DateFormat entryFormat; 190 String startDay; 191 boolean hasComments = false; 192 int maxEntries; 193 String weblogName; 194 195 if( (weblogName = params.get(PARAM_PAGE)) == null ) { 196 weblogName = context.getPage().getName(); 197 } 198 199 if( (days = context.getHttpParameter( "weblog."+PARAM_DAYS )) == null ) { 200 days = params.get( PARAM_DAYS ); 201 } 202 203 if( ( params.get(PARAM_ENTRYFORMAT)) == null ) { 204 entryFormat = Preferences.getDateFormat( context, TimeFormat.DATETIME ); 205 } else { 206 entryFormat = new SimpleDateFormat( params.get(PARAM_ENTRYFORMAT) ); 207 } 208 209 if( days != null ) { 210 if( days.equalsIgnoreCase("all") ) { 211 numDays = Integer.MAX_VALUE; 212 } else { 213 numDays = TextUtil.parseIntParameter( days, DEFAULT_DAYS ); 214 } 215 } 216 217 218 if( (startDay = params.get(PARAM_STARTDATE)) == null ) { 219 startDay = context.getHttpParameter( "weblog."+PARAM_STARTDATE ); 220 } 221 if ("ddMMyy".equalsIgnoreCase(startDay)) { 222 //if using the default snippet, this value is clearly the default 223 //and is not valid 224 startDay = null; 225 } 226 227 if( TextUtil.isPositive( params.get(PARAM_ALLOWCOMMENTS) ) ) { 228 hasComments = true; 229 } 230 231 maxEntries = TextUtil.parseIntParameter( params.get(PARAM_MAXENTRIES), Integer.MAX_VALUE ); 232 233 // 234 // Determine the date range which to include. 235 // 236 startTime = Calendar.getInstance(); 237 stopTime = Calendar.getInstance(); 238 239 if( startDay != null ) { 240 final SimpleDateFormat fmt = new SimpleDateFormat( DEFAULT_DATEFORMAT ); 241 try { 242 final Date d = fmt.parse( startDay ); 243 startTime.setTime( d ); 244 stopTime.setTime( d ); 245 } catch( final ParseException e ) { 246 return "Illegal time format: "+ TextUtil.replaceEntities(startDay); 247 } 248 } 249 250 // 251 // Mark this to be a weblog 252 // 253 context.getPage().setAttribute(ATTR_ISWEBLOG, "true"); 254 255 // 256 // We make a wild guess here that nobody can do millisecond accuracy here. 257 // 258 startTime.add( Calendar.DAY_OF_MONTH, -numDays ); 259 startTime.set( Calendar.HOUR, 0 ); 260 startTime.set( Calendar.MINUTE, 0 ); 261 startTime.set( Calendar.SECOND, 0 ); 262 stopTime.set( Calendar.HOUR, 23 ); 263 stopTime.set( Calendar.MINUTE, 59 ); 264 stopTime.set( Calendar.SECOND, 59 ); 265 266 final StringBuilder sb = new StringBuilder(); 267 final List< Page > blogEntries = findBlogEntries( engine, weblogName, startTime.getTime(), stopTime.getTime() ); 268 blogEntries.sort( new PageDateComparator() ); 269 270 sb.append("<div class=\"weblog\">\n"); 271 272 for( final Iterator< Page > i = blogEntries.iterator(); i.hasNext() && maxEntries-- > 0 ; ) { 273 final Page p = i.next(); 274 if( mgr.checkPermission( context.getWikiSession(), new PagePermission(p, PagePermission.VIEW_ACTION) ) ) { 275 addEntryHTML( context, entryFormat, hasComments, sb, p, params ); 276 } 277 } 278 279 sb.append("</div>\n"); 280 281 return sb.toString(); 282 } 283 284 /** 285 * Generates HTML for an entry. 286 * 287 * @param context 288 * @param entryFormat 289 * @param hasComments True, if comments are enabled. 290 * @param buffer The buffer to which we add. 291 * @param entry 292 * @throws ProviderException 293 */ 294 private void addEntryHTML( final Context context, final DateFormat entryFormat, final boolean hasComments, 295 final StringBuilder buffer, final Page entry, final Map< String, String > params) { 296 final Engine engine = context.getEngine(); 297 final ResourceBundle rb = Preferences.getBundle(context, Plugin.CORE_PLUGINS_RESOURCEBUNDLE); 298 299 buffer.append("<div class=\"weblogentry\">\n"); 300 301 // 302 // Heading 303 // 304 buffer.append("<div class=\"weblogentryheading\">\n"); 305 306 final Date entryDate = entry.getLastModified(); 307 buffer.append( entryFormat != null ? entryFormat.format(entryDate) : entryDate ); 308 buffer.append("</div>\n"); 309 310 // 311 // Append the text of the latest version. Reset the context to that page. 312 // 313 final Context entryCtx = context.clone(); 314 entryCtx.setPage( entry ); 315 316 String html = engine.getManager( RenderingManager.class ).getHTML( entryCtx, engine.getManager( PageManager.class ).getPage( entry.getName() ) ); 317 318 // Extract the first h1/h2/h3 as title, and replace with null 319 buffer.append("<div class=\"weblogentrytitle\">\n"); 320 final Matcher matcher = HEADINGPATTERN.matcher( html ); 321 if ( matcher.find() ) { 322 final String title = matcher.group(2); 323 html = matcher.replaceFirst(""); 324 buffer.append( title ); 325 } else { 326 buffer.append( entry.getName() ); 327 } 328 buffer.append("</div>\n"); 329 buffer.append("<div class=\"weblogentrybody\">\n"); 330 331 final int preview = TextUtil.parseIntParameter(params.get(PARAM_PREVIEW), 0); 332 if (preview > 0) { 333 // 334 // We start with the first 'preview' number of characters from the text, 335 // and then add characters to it until we get to a linebreak. 336 // The idea is that cutting off at a linebreak is less likely 337 // to disturb the HTML and leave us with garbled output. 338 // 339 boolean hasBeenCutOff = false; 340 int cutoff = Math.min(preview, html.length()); 341 while (cutoff < html.length()) { 342 if (html.charAt(cutoff) == '\r' || html.charAt(cutoff) == '\n') { 343 hasBeenCutOff = true; 344 break; 345 } 346 cutoff++; 347 } 348 buffer.append( html, 0, cutoff ); 349 if (hasBeenCutOff) { 350 buffer.append( " <a href=\"" ).append( entryCtx.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), entry.getName() ) ).append( "\">" ).append( rb.getString( "weblogentryplugin.more" ) ).append( "</a>\n" ); 351 } 352 } else { 353 buffer.append(html); 354 } 355 buffer.append("</div>\n"); 356 357 // 358 // Append footer 359 // 360 buffer.append("<div class=\"weblogentryfooter\">\n"); 361 362 String author = entry.getAuthor(); 363 364 if( author != null ) { 365 if( engine.getManager( PageManager.class ).wikiPageExists(author) ) { 366 author = "<a href=\""+entryCtx.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), author )+"\">"+engine.getManager( RenderingManager.class ).beautifyTitle(author)+"</a>"; 367 } 368 } else { 369 author = "AnonymousCoward"; 370 } 371 372 buffer.append( MessageFormat.format( rb.getString( "weblogentryplugin.postedby" ), author ) ); 373 buffer.append( "<a href=\"" ).append( entryCtx.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), entry.getName() ) ).append( "\">" ).append( rb.getString( "weblogentryplugin.permalink" ) ).append( "</a>" ); 374 final String commentPageName = TextUtil.replaceString( entry.getName(), "blogentry", "comments" ); 375 376 if( hasComments ) { 377 final int numComments = guessNumberOfComments( engine, commentPageName ); 378 379 // 380 // We add the number of comments to the URL so that the user's browsers would realize that the page has changed. 381 // 382 buffer.append( " " ); 383 384 final String addcomment = rb.getString("weblogentryplugin.addcomment"); 385 386 buffer.append( "<a href=\"" ).append( entryCtx.getURL( ContextEnum.PAGE_COMMENT.getRequestContext(), commentPageName, "nc=" + numComments ) ).append( "\">" ).append( MessageFormat.format( addcomment, numComments ) ).append( "</a>" ); 387 } 388 389 buffer.append("</div>\n"); 390 391 // Done, close 392 buffer.append("</div>\n"); 393 } 394 395 private int guessNumberOfComments( final Engine engine, final String commentpage ) { 396 final String pagedata = engine.getManager( PageManager.class ).getPureText( commentpage, WikiProvider.LATEST_VERSION ); 397 if( pagedata == null || pagedata.trim().isEmpty() ) { 398 return 0; 399 } 400 401 return TextUtil.countSections( pagedata ); 402 } 403 404 /** 405 * Attempts to locate all pages that correspond to the 406 * blog entry pattern. Will only consider the days on the dates; not the hours and minutes. 407 * 408 * @param engine Engine which is used to get the pages 409 * @param baseName The basename (e.g. "Main" if you want "Main_blogentry_xxxx") 410 * @param start The date which is the first to be considered 411 * @param end The end date which is the last to be considered 412 * @return a list of pages with their FIRST revisions. 413 */ 414 public List< Page > findBlogEntries( final Engine engine, String baseName, final Date start, final Date end ) { 415 final PageManager mgr = engine.getManager( PageManager.class ); 416 final Set< String > allPages = engine.getManager( ReferenceManager.class ).findCreated(); 417 final ArrayList< Page > result = new ArrayList<>(); 418 419 baseName = makeEntryPage( baseName ); 420 421 for( final String pageName : allPages ) { 422 if( pageName.startsWith( baseName ) ) { 423 try { 424 final Page firstVersion = mgr.getPageInfo( pageName, 1 ); 425 final Date d = firstVersion.getLastModified(); 426 427 if( d.after( start ) && d.before( end ) ) { 428 result.add( firstVersion ); 429 } 430 } catch( final Exception e ) { 431 LOG.debug( "Page name :" + pageName + " was suspected as a blog entry but it isn't because of parsing errors", e ); 432 } 433 } 434 } 435 436 return result; 437 } 438 439 /** 440 * Reverse comparison. 441 */ 442 private static class PageDateComparator implements Comparator< Page > { 443 444 /**{@inheritDoc}*/ 445 @Override 446 public int compare( final Page page1, final Page page2 ) { 447 if( page1 == null || page2 == null ) { 448 return 0; 449 } 450 return page2.getLastModified().compareTo( page1.getLastModified() ); 451 } 452 453 } 454 455 /** 456 * Mark us as being a real weblog. 457 * 458 * {@inheritDoc} 459 */ 460 @Override 461 public void executeParser( final PluginElement element, final Context context, final Map< String, String > params ) { 462 context.getPage().setAttribute( ATTR_ISWEBLOG, "true" ); 463 } 464 465}