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