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}