    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        if (!rssFile.getParentFile().exists()) {
    094            if (!rssFile.getParentFile().mkdirs()) {
    095                LOG.warn("Failed to mkdirs at " + rssFile.getParentFile().getAbsolutePath() + " rss feeds will probably fail");
    096            }
    097        }
    098        final int rssInterval = TextUtil.getIntegerProperty( properties, DefaultRSSGenerator.PROP_INTERVAL, 3600 );
    099        final RSSThread rssThread = new RSSThread( engine, rssFile, rssInterval );
    100        rssThread.start();
    101    }
    102
    103    private String getAuthor( final Page page ) {
    104        String author = page.getAuthor();
    105        if( author == null ) {
    106            author = "An unknown author";
    107        }
    108
    109        return author;
    110    }
    111
    112    private String getAttachmentDescription( final Attachment att ) {
    113        final String author = getAuthor( att );
    114        final StringBuilder sb = new StringBuilder();
    115
    116        if( att.getVersion() != 1 ) {
    117            sb.append( author ).append( " uploaded a new version of this attachment on " ).append( att.getLastModified() );
    118        } else {
    119            sb.append( author ).append( " created this attachment on " ).append( att.getLastModified() );
    120        }
    121
    122        sb.append( "<br /><hr /><br />" )
    123          .append( "Parent page: <a href=\"" )
    124          .append( m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), att.getParentName(), null ) )
    125          .append( "\">" ).append( att.getParentName() ).append( "</a><br />" )
    126          .append( "Info page: <a href=\"" )
    127          .append( m_engine.getURL( ContextEnum.PAGE_INFO.getRequestContext(), att.getName(), null ) )
    128          .append( "\">" ).append( att.getName() ).append( "</a>" );
    129
    130        return sb.toString();
    131    }
    132
    133    private String getPageDescription( final Page page ) {
    134        final StringBuilder buf = new StringBuilder();
    135        final String author = getAuthor( page );
    136        final Context ctx = Wiki.context().create( m_engine, page );
    137        if( page.getVersion() > 1 ) {
    138            final String diff = m_engine.getManager( DifferenceManager.class ).getDiff( ctx,
    139                                                                page.getVersion() - 1, // FIXME: Will fail when non-contiguous versions
    140                                                                         page.getVersion() );
    141
    142            buf.append( author ).append( " changed this page on " ).append( page.getLastModified() ).append( ":<br /><hr /><br />" );
    143            buf.append( diff );
    144        } else {
    145            buf.append( author ).append( " created this page on " ).append( page.getLastModified() ).append( ":<br /><hr /><br />" );
    146            buf.append( m_engine.getManager( RenderingManager.class ).getHTML( page.getName() ) );
    147        }
    148
    149        return buf.toString();
    150    }
    151
    152    private String getEntryDescription( final Page page ) {
    153        final String res;
    154        if( page instanceof Attachment ) {
    155            res = getAttachmentDescription( (Attachment)page );
    156        } else {
    157            res = getPageDescription( page );
    158        }
    159
    160        return res;
    161    }
    162
    163    // FIXME: This should probably return something more intelligent
    164    private String getEntryTitle( final Page page ) {
    165        return page.getName() + ", version " + page.getVersion();
    166    }
    167
    168    /** {@inheritDoc} */
    169    @Override
    170    public String generate() {
    171        final Context context = Wiki.context().create( m_engine, Wiki.contents().page( m_engine, "__DUMMY" ) );
    172        context.setRequestContext( ContextEnum.PAGE_RSS.getRequestContext() );
    173        final Feed feed = new RSS10Feed( context );
    174        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + generateFullWikiRSS( context, feed );
    175    }
    176
    177    /** {@inheritDoc} */
    178    @Override
    179    public String generateFeed( final Context wikiContext, final List< Page > changed, final String mode, final String type ) throws IllegalArgumentException {
    180        final Feed feed;
    181        final String res;
    182
    183        if( ATOM.equals(type) ) {
    184            feed = new AtomFeed( wikiContext );
    185        } else if( RSS20.equals( type ) ) {
    186            feed = new RSS20Feed( wikiContext );
    187        } else {
    188            feed = new RSS10Feed( wikiContext );
    189        }
    190
    191        feed.setMode( mode );
    192
    193        if( MODE_BLOG.equals( mode ) ) {
    194            res = generateBlogRSS( wikiContext, changed, feed );
    195        } else if( MODE_FULL.equals(mode) ) {
    196            res = generateFullWikiRSS( wikiContext, feed );
    197        } else if( MODE_WIKI.equals(mode) ) {
    198            res = generateWikiPageRSS( wikiContext, changed, feed );
    199        } else {
    200            throw new IllegalArgumentException( "Invalid value for feed mode: "+mode );
    201        }
    202
    203        return res;
    204    }
    205
    206    /** {@inheritDoc} */
    207    @Override
    208    public synchronized boolean isEnabled() {
    209        return m_enabled;
    210    }
    211
    212    /** {@inheritDoc} */
    213    @Override
    214    public synchronized void setEnabled( final boolean enabled ) {
    215        m_enabled = enabled;
    216    }
    217
    218    /** {@inheritDoc} */
    219    @Override
    220    public String getRssFile() {
    221        return m_rssFile;
    222    }
    223
    224    /** {@inheritDoc} */
    225    @Override
    226    public String generateFullWikiRSS( final Context wikiContext, final Feed feed ) {
    227        feed.setChannelTitle( m_engine.getApplicationName() );
    228        feed.setFeedURL( m_engine.getBaseURL() );
    229        feed.setChannelLanguage( m_channelLanguage );
    230        feed.setChannelDescription( m_channelDescription );
    231
    232        final Set< Page > changed = m_engine.getManager( PageManager.class ).getRecentChanges();
    233
    234        final Session session = Wiki.session().guest( m_engine );
    235        int items = 0;
    236        for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) {
    237            final Page page = i.next();
    238
    239            //  Check if the anonymous user has view access to this page.
    240            if( !m_engine.getManager( AuthorizationManager.class ).checkPermission(session, new PagePermission(page,PagePermission.VIEW_ACTION) ) ) {
    241                // No permission, skip to the next one.
    242                continue;
    243            }
    244
    245            final String url;
    246            if( page instanceof Attachment ) {
    247                url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(),null );
    248            } else {
    249                url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(), null );
    250            }
    251
    252            final Entry e = new Entry();
    253            e.setPage( page );
    254            e.setURL( url );
    255            e.setTitle( page.getName() );
    256            e.setContent( getEntryDescription(page) );
    257            e.setAuthor( getAuthor(page) );
    258
    259            feed.addEntry( e );
    260        }
    261
    262        return feed.getString();
    263    }
    264
    265    /** {@inheritDoc} */
    266    @Override
    267    public String generateWikiPageRSS( final Context wikiContext, final List< Page > changed, final Feed feed ) {
    268        feed.setChannelTitle( m_engine.getApplicationName()+": "+wikiContext.getPage().getName() );
    269        feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) );
    270        final String language = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_LANGUAGE );
    271
    272        if( language != null ) {
    273            feed.setChannelLanguage( language );
    274        } else {
    275            feed.setChannelLanguage( m_channelLanguage );
    276        }
    277        final String channelDescription = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION );
    278
    279        if( channelDescription != null ) {
    280            feed.setChannelDescription( channelDescription );
    281        }
    282
    283        changed.sort( new PageTimeComparator() );
    284
    285        int items = 0;
    286        for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) {
    287            final Page page = i.next();
    288            final Entry e = new Entry();
    289            e.setPage( page );
    290            String url;
    291
    292            if( page instanceof Attachment ) {
    293                url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(), "version=" + page.getVersion() );
    294            } else {
    295                url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(), "version=" + page.getVersion() );
    296            }
    297
    298            // Unfortunately, this is needed because the code will again go through replacement conversion
    299            url = TextUtil.replaceString( url, "&amp;", "&" );
    300            e.setURL( url );
    301            e.setTitle( getEntryTitle(page) );
    302            e.setContent( getEntryDescription(page) );
    303            e.setAuthor( getAuthor(page) );
    304
    305            feed.addEntry( e );
    306        }
    307
    308        return feed.getString();
    309    }
    310
    311
    312    /** {@inheritDoc} */
    313    @Override
    314    public String generateBlogRSS( final Context wikiContext, final List< Page > changed, final Feed feed ) {
    315        LOG.debug( "Generating RSS for blog, size={}", changed.size() );
    316
    317        final String ctitle = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_TITLE );
    318        feed.setChannelTitle(Objects.requireNonNullElseGet(ctitle, () -> m_engine.getApplicationName() + ":" + wikiContext.getPage().getName()));
    319
    320        feed.setFeedURL( wikiContext.getViewURL( wikiContext.getPage().getName() ) );
    321
    322        final String language = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_LANGUAGE );
    323        if( language != null ) {
    324            feed.setChannelLanguage( language );
    325        } else {
    326            feed.setChannelLanguage( m_channelLanguage );
    327        }
    328
    329        final String channelDescription = m_engine.getManager( VariableManager.class ).getVariable( wikiContext, PROP_CHANNEL_DESCRIPTION );
    330        if( channelDescription != null ) {
    331            feed.setChannelDescription( channelDescription );
    332        }
    333
    334        changed.sort( new PageTimeComparator() );
    335
    336        int items = 0;
    337        for( final Iterator< Page > i = changed.iterator(); i.hasNext() && items < 15; items++ ) {
    338            final Page page = i.next();
    339            final Entry e = new Entry();
    340            e.setPage( page );
    341            final String url;
    342
    343            if( page instanceof Attachment ) {
    344                url = m_engine.getURL( ContextEnum.PAGE_ATTACH.getRequestContext(), page.getName(),null );
    345            } else {
    346                url = m_engine.getURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(),null );
    347            }
    348
    349            e.setURL( url );
    350
    351            //  Title
    352            String pageText = m_engine.getManager( PageManager.class ).getPureText( page.getName(), WikiProvider.LATEST_VERSION );
    353
    354            String title = "";
    355            final int firstLine = pageText.indexOf('\n');
    356
    357            if( firstLine > 0 ) {
    358                title = pageText.substring( 0, firstLine ).trim();
    359            }
    360
    361            if( title.isEmpty() ) {
    362                title = page.getName();
    363            }
    364
    365            // Remove wiki formatting
    366            while( title.startsWith("!") ) {
    367                title = title.substring(1);
    368            }
    369
    370            e.setTitle( title );
    371
    372            //  Description
    373            if( firstLine > 0 ) {
    374                int maxlen = pageText.length();
    375                if( maxlen > MAX_CHARACTERS ) {
    376                    maxlen = MAX_CHARACTERS;
    377                }
    378                pageText = m_engine.getManager( RenderingManager.class ).textToHTML( wikiContext, pageText.substring( firstLine + 1, maxlen ).trim() );
    379                if( maxlen == MAX_CHARACTERS ) {
    380                    pageText += "...";
    381                }
    382                e.setContent( pageText );
    383            } else {
    384                e.setContent( title );
    385            }
    386            e.setAuthor( getAuthor(page) );
    387            feed.addEntry( e );
    388        }
    389
    390        return feed.getString();
    391    }
    392
    393}