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}