    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.rss;
    020
    021import org.apache.logging.log4j.LogManager;
    022import org.apache.logging.log4j.Logger;
    023import org.apache.wiki.api.core.Attachment;
    024import org.apache.wiki.api.core.Context;
    025import org.apache.wiki.api.core.ContextEnum;
    026import org.apache.wiki.api.core.Engine;
    027import org.apache.wiki.api.core.Page;
    028import org.apache.wiki.api.core.Session;
    029import org.apache.wiki.api.providers.WikiProvider;
    030import org.apache.wiki.api.spi.Wiki;
    031import org.apache.wiki.auth.AuthorizationManager;
    032import org.apache.wiki.auth.permissions.PagePermission;
    033import org.apache.wiki.diff.DifferenceManager;
    034import org.apache.wiki.pages.PageManager;
    035import org.apache.wiki.pages.PageTimeComparator;
    036import org.apache.wiki.render.RenderingManager;
    037import org.apache.wiki.util.TextUtil;
    038import org.apache.wiki.variables.VariableManager;
    039
    040import java.io.File;
    041import java.util.Iterator;
    042import java.util.List;
    043import java.util.Objects;
    044import java.util.Properties;
    045import java.util.Set;
    046
    047
    048/**
    049 * Default implementation for {@link RSSGenerator}.
    050 *
    051 * {@inheritDoc}
    052 */
    053// FIXME: Limit diff and page content size.
    054public class DefaultRSSGenerator implements RSSGenerator {
    055
    056    private static final Logger LOG = LogManager.getLogger( DefaultRSSGenerator.class );
    057    private final Engine m_engine;
    058
    059    /** The RSS file to generate. */
    060    private final String m_rssFile;
    061    private String m_channelDescription = "";
    062    private String m_channelLanguage = "en-us";
    063    private boolean m_enabled = true;
    064
    065    private static final int MAX_CHARACTERS = Integer.MAX_VALUE-1;
    066
    067    /**
    068     *  Builds the RSS generator for a given Engine.
    069     *
    070     *  @param engine The Engine.
    071     *  @param properties The properties.
    072     */
    073    public DefaultRSSGenerator( final Engine engine, final Properties properties ) {
    074        m_engine = engine;
    075        m_channelDescription = properties.getProperty( PROP_CHANNEL_DESCRIPTION, m_channelDescription );
    076        m_channelLanguage = properties.getProperty( PROP_CHANNEL_LANGUAGE, m_channelLanguage );
    077        m_rssFile = TextUtil.getStringProperty( properties, DefaultRSSGenerator.PROP_RSSFILE, "rss.rdf" );
    078    }
    079
    080    /**
    081     * {@inheritDoc}
    082     *
    083     * Start the RSS generator & generator thread
    084     */
    085    @Override
    086    public void initialize( final Engine engine, final Properties properties ) {
    087        final File rssFile;
    088        if( m_rssFile.startsWith( File.separator ) ) { // honor absolute pathnames
    089            rssFile = new File( m_rssFile );
    090        } else { // relative path names are anchored from the webapp root path
    091            rssFile = new File( engine.getRootPath(), m_rssFile );
    092        }
    093        final int rssInterval = TextUtil.getIntegerProperty( properties, DefaultRSSGenerator.PROP_INTERVAL, 3600 );
    094        final RSSThread rssThread = new RSSThread( engine, rssFile, rssInterval );
    095        rssThread.start();
    096    }
    097
    098    private String getAuthor( final Page page ) {
    099        String author = page.getAuthor();
    100        if( author == null ) {
    101            author = "An unknown author";
    102        }
    103
    104        return author;
    105    }
    106
    107    private String getAttachmentDescription( final Attachment att ) {
    108        final String author = getAuthor( att );
    109        final StringBuilder sb = new StringBuilder();
    110
    111        if( att.getVersion() != 1 ) {
    112            sb.append( author ).append( " uploaded a new version of this attachment on " ).append( att.getLastModified() );
    113        } else {
    114            sb.append( author ).append( " created this attachment on " ).append( att.getLastModified() );
    115        }
    116
    117        sb.append( "<br /><hr /><br />" )
    118          .append( "Parent page: <a href=\"" )
    119          .append( m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), att.getParentName(), null ) )
    120          .append( "\">" ).append( att.getParentName() ).append( "</a><br />" )
    121          .append( "Info page: <a href=\"" )
    122          .append( m_engine.getURL( ContextEnum.PAGE_INFO.getRequestContext(), att.getName(), null ) )
    123          .append( "\">" ).append( att.getName() ).append( "</a>" );
    124
    125        return sb.toString();
    126    }
    127
    128    private String getPageDescription( final Page page ) {
    129        final StringBuilder buf = new StringBuilder();
    130        final String author = getAuthor( page );
    131        final Context ctx = Wiki.context().create( m_engine, page );
    132        if( page.getVersion() > 1 ) {
    133            final String diff = m_engine.getManager( DifferenceManager.class ).getDiff( ctx,
    134                                                                page.getVersion() - 1, // FIXME: Will fail when non-contiguous versions
    135                                                                         page.getVersion() );
    136
    137            buf.append( author ).append( " changed this page on " ).append( page.getLastModified() ).append( ":<br /><hr /><br />" );
    138            buf.append( diff );
    139        } else {
    140            buf.append( author ).append( " created this page on " ).append( page.getLastModified() ).append( ":<br /><hr /><br />" );
    141            buf.append( m_engine.getManager( RenderingManager.class ).getHTML( page.getName() ) );
    142        }
    143
    144        return buf.toString();
    145    }
    146
    147    private String getEntryDescription( final Page page ) {
    148        final String res;
    149        if( page instanceof Attachment ) {
    150            res = getAttachmentDescription( (Attachment)page );
    151        } else {
    152            res = getPageDescription( page );
    153        }
    154
    155        return res;
    156    }
    157
    158    // FIXME: This should probably return something more intelligent
    159    private String getEntryTitle( final Page page ) {
    160        return page.getName() + ", version " + page.getVersion();
    161    }
    162
    163    /** {@inheritDoc} */
    164    @Override
    165    public String generate() {
    166        final Context context = Wiki.context().create( m_engine, Wiki.contents().page( m_engine, "__DUMMY" ) );
    167        context.setRequestContext( ContextEnum.PAGE_RSS.getRequestContext() );
    168        final Feed feed = new RSS10Feed( context );
    169        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + generateFullWikiRSS( context, feed );
    170    }
    171
    172    /** {@inheritDoc} */
    173    @Override
    174    public String generateFeed( final Context wikiContext, final List< Page > changed, final String mode, final String type ) throws IllegalArgumentException {
    175        final Feed feed;
    176        final String res;
    177
    178        if( ATOM.equals(type) ) {
    179            feed = new AtomFeed( wikiContext );
    180        } else if( RSS20.equals( type ) ) {
    181            feed = new RSS20Feed( wikiContext );
    182        } else {
    183            feed = new RSS10Feed( wikiContext );
    184        }
    185
    186        feed.setMode( mode );
    187
    188        if( MODE_BLOG.equals( mode ) ) {
    189            res = generateBlogRSS( wikiContext, changed, feed );
    190        } else if( MODE_FULL.equals(mode) ) {
    191            res = generateFullWikiRSS( wikiContext, feed );
    192        } else if( MODE_WIKI.equals(mode) ) {
    193            res = generateWikiPageRSS( wikiContext, changed, feed );
    194        } else {
    195            throw new IllegalArgumentException( "Invalid value for feed mode: "+mode );
    196        }
    197
    198        return res;
    199    }
    200
    201    /** {@inheritDoc} */
    202    @Override
    203    public synchronized boolean isEnabled() {
    204        return m_enabled;
    205    }
    206
    207    /** {@inheritDoc} */
    208    @Override
    209    public synchronized void setEnabled( final boolean enabled ) {
    210        m_enabled = enabled;
    211    }
    212
    213    /** {@inheritDoc} */
    214    @Override
    215    public String getRssFile() {
    216        return m_rssFile;
    217    }
    218
    219    /** {@inheritDoc} */
    220    @Override
    221    public String generateFullWikiRSS( final Context wikiContext, final Feed feed ) {
    222        feed.setChannelTitle( m_engine.getApplicationName() );
    223        feed.setFeedURL( m_engine.getBaseURL() );
    224        feed.setChannelLanguage( m_channelLanguage );
    225        feed.setChannelDescription( m_channelDescription );
    226
    227        final Set< Page > changed = m_engine.getManager( PageManager.class ).getRecentChanges();
    228
    229        final Session session = Wiki.session().guest( m_engine );
    230        int items = 0;
    231        for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) {
    232            final Page page = i.next();
    233
    234            //  Check if the anonymous user has view access to this page.
    235            if( !m_engine.getManager( AuthorizationManager.class ).checkPermission(session, new PagePermission(page,PagePermission.VIEW_ACTION) ) ) {
    236                // No permission, skip to the next one.
    237                continue;
    238            }
    239
    240            final String url;
    241            if( page instanceof Attachment ) {
    242                url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(),null );
    243            } else {
    244                url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(), null );
    245            }
    246
    247            final Entry e = new Entry();
    248            e.setPage( page );
    249            e.setURL( url );
    250            e.setTitle( page.getName() );
    251            e.setContent( getEntryDescription(page) );
    252            e.setAuthor( getAuthor(page) );
    253
    254            feed.addEntry( e );
    255        }
    256
    257        return feed.getString();
    258    }
    259
    260    /** {@inheritDoc} */
    261    @Override
    262    public String generateWikiPageRSS( final Context wikiContext, final List< Page > changed, final Feed feed ) {
    263        feed.setChannelTitle( m_engine.getApplicationName()+": "+wikiContext.getPage().getName() );
    264        feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) );
    265        final String language = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_LANGUAGE );
    266
    267        if( language != null ) {
    268            feed.setChannelLanguage( language );
    269        } else {
    270            feed.setChannelLanguage( m_channelLanguage );
    271        }
    272        final String channelDescription = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION );
    273
    274        if( channelDescription != null ) {
    275            feed.setChannelDescription( channelDescription );
    276        }
    277
    278        changed.sort( new PageTimeComparator() );
    279
    280        int items = 0;
    281        for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) {
    282            final Page page = i.next();
    283            final Entry e = new Entry();
    284            e.setPage( page );
    285            String url;
    286
    287            if( page instanceof Attachment ) {
    288                url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(), "version=" + page.getVersion() );
    289            } else {
    290                url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(), "version=" + page.getVersion() );
    291            }
    292
    293            // Unfortunately, this is needed because the code will again go through replacement conversion
    294            url = TextUtil.replaceString( url, "&amp;", "&" );
    295            e.setURL( url );
    296            e.setTitle( getEntryTitle(page) );
    297            e.setContent( getEntryDescription(page) );
    298            e.setAuthor( getAuthor(page) );
    299
    300            feed.addEntry( e );
    301        }
    302
    303        return feed.getString();
    304    }
    305
    306
    307    /** {@inheritDoc} */
    308    @Override
    309    public String generateBlogRSS( final Context wikiContext, final List< Page > changed, final Feed feed ) {
    310        LOG.debug( "Generating RSS for blog, size={}", changed.size() );
    311
    312        final String ctitle = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_TITLE );
    313        feed.setChannelTitle(Objects.requireNonNullElseGet(ctitle, () -> m_engine.getApplicationName() + ":" + wikiContext.getPage().getName()));
    314
    315        feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) );
    316
    317        final String language = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_LANGUAGE );
    318        if( language != null ) {
    319            feed.setChannelLanguage( language );
    320        } else {
    321            feed.setChannelLanguage( m_channelLanguage );
    322        }
    323
    324        final String channelDescription = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION );
    325        if( channelDescription != null ) {
    326            feed.setChannelDescription( channelDescription );
    327        }
    328
    329        changed.sort( new PageTimeComparator() );
    330
    331        int items = 0;
    332        for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) {
    333            final Page page = i.next();
    334            final Entry e = new Entry();
    335            e.setPage( page );
    336            final String url;
    337
    338            if( page instanceof Attachment ) {
    339                url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(),null );
    340            } else {
    341                url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(),null );
    342            }
    343
    344            e.setURL( url );
    345
    346            //  Title
    347            String pageText = m_engine.getManager( PageManager.class ).getPureText( page.getName(), WikiProvider.LATEST_VERSION );
    348
    349            String title = "";
    350            final int firstLine = pageText.indexOf('\n');
    351
    352            if( firstLine > 0 ) {
    353                title = pageText.substring( 0, firstLine ).trim();
    354            }
    355
    356            if( title.isEmpty() ) {
    357                title = page.getName();
    358            }
    359
    360            // Remove wiki formatting
    361            while( title.startsWith("!") ) {
    362                title = title.substring(1);
    363            }
    364
    365            e.setTitle( title );
    366
    367            //  Description
    368            if( firstLine > 0 ) {
    369                int maxlen = pageText.length();
    370                if( maxlen > MAX_CHARACTERS ) {
    371                    maxlen = MAX_CHARACTERS;
    372                }
    373                pageText = m_engine.getManager( RenderingManager.class ).textToHTML( wikiContext, pageText.substring( firstLine + 1, maxlen ).trim() );
    374                if( maxlen == MAX_CHARACTERS ) {
    375                    pageText += "...";
    376                }
    377                e.setContent( pageText );
    378            } else {
    379                e.setContent( title );
    380            }
    381            e.setAuthor( getAuthor(page) );
    382            feed.addEntry( e );
    383        }
    384
    385        return feed.getString();
    386    }
    387
    388}