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