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