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