    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( "&nbsp;&nbsp;" );
    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}