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