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.filters; 020 021import org.apache.log4j.Logger; 022import org.apache.wiki.WikiContext; 023import org.apache.wiki.WikiEngine; 024import org.apache.wiki.api.engine.FilterManager; 025import org.apache.wiki.api.exceptions.FilterException; 026import org.apache.wiki.api.exceptions.WikiException; 027import org.apache.wiki.api.filters.PageFilter; 028import org.apache.wiki.event.WikiEventManager; 029import org.apache.wiki.event.WikiPageEvent; 030import org.apache.wiki.modules.ModuleManager; 031import org.apache.wiki.modules.WikiModuleInfo; 032import org.apache.wiki.util.ClassUtil; 033import org.apache.wiki.util.PriorityList; 034import org.apache.wiki.util.XmlUtil; 035import org.jdom2.Element; 036 037import java.io.File; 038import java.io.FileInputStream; 039import java.io.IOException; 040import java.io.InputStream; 041import java.util.Collection; 042import java.util.HashMap; 043import java.util.Iterator; 044import java.util.List; 045import java.util.Map; 046import java.util.Properties; 047 048 049/** 050 * Manages the page filters. Page filters are components that can be executed 051 * at certain places: 052 * <ul> 053 * <li>Before the page is translated into HTML. 054 * <li>After the page has been translated into HTML. 055 * <li>Before the page is saved. 056 * <li>After the page has been saved. 057 * </ul> 058 * 059 * Using page filters allows you to modify the page data on-the-fly, and do things like 060 * adding your own custom WikiMarkup. 061 * 062 * <p> 063 * The initial page filter configuration is kept in a file called "filters.xml". The 064 * format is really very simple: 065 * <pre> 066 * <?xml version="1.0"?> 067 * 068 * <pagefilters> 069 * 070 * <filter> 071 * <class>org.apache.wiki.filters.ProfanityFilter</class> 072 * </filter> 073 * 074 * <filter> 075 * <class>org.apache.wiki.filters.TestFilter</class> 076 * 077 * <param> 078 * <name>foobar</name> 079 * <value>Zippadippadai</value> 080 * </param> 081 * 082 * <param> 083 * <name>blatblaa</name> 084 * <value>5</value> 085 * </param> 086 * 087 * </filter> 088 * </pagefilters> 089 * </pre> 090 * 091 * The <filter> -sections define the filters. For more information, please see 092 * the PageFilterConfiguration page in the JSPWiki distribution. 093 */ 094public class DefaultFilterManager extends ModuleManager implements FilterManager { 095 096 private PriorityList< PageFilter > m_pageFilters = new PriorityList< PageFilter >(); 097 098 private Map< String, PageFilterInfo > m_filterClassMap = new HashMap< String, PageFilterInfo >(); 099 100 private static final Logger log = Logger.getLogger(DefaultFilterManager.class); 101 102 /** 103 * Constructs a new FilterManager object. 104 * 105 * @param engine The WikiEngine which owns the FilterManager 106 * @param props Properties to initialize the FilterManager with 107 * @throws WikiException If something goes wrong. 108 */ 109 public DefaultFilterManager( WikiEngine engine, Properties props ) 110 throws WikiException 111 { 112 super( engine ); 113 initialize( props ); 114 } 115 116 /** 117 * Adds a page filter to the queue. The priority defines in which 118 * order the page filters are run, the highest priority filters go 119 * in the queue first. 120 * <p> 121 * In case two filters have the same priority, their execution order 122 * is the insertion order. 123 * 124 * @since 2.1.44. 125 * @param f PageFilter to add 126 * @param priority The priority in which position to add it in. 127 * @throws IllegalArgumentException If the PageFilter is null or invalid. 128 */ 129 public void addPageFilter( PageFilter f, int priority ) throws IllegalArgumentException 130 { 131 if( f == null ) 132 { 133 throw new IllegalArgumentException("Attempt to provide a null filter - this should never happen. Please check your configuration (or if you're a developer, check your own code.)"); 134 } 135 136 m_pageFilters.add( f, priority ); 137 } 138 139 private void initPageFilter( String className, Properties props ) 140 { 141 try 142 { 143 PageFilterInfo info = m_filterClassMap.get( className ); 144 145 if( info != null && !checkCompatibility(info) ) 146 { 147 String msg = "Filter '"+info.getName()+"' not compatible with this version of JSPWiki"; 148 log.warn(msg); 149 return; 150 } 151 152 int priority = 0; // FIXME: Currently fixed. 153 154 Class< ? > cl = ClassUtil.findClass( "org.apache.wiki.filters", className ); 155 156 PageFilter filter = (PageFilter)cl.newInstance(); 157 158 filter.initialize( m_engine, props ); 159 160 addPageFilter( filter, priority ); 161 log.info("Added page filter "+cl.getName()+" with priority "+priority); 162 } 163 catch( ClassNotFoundException e ) 164 { 165 log.error("Unable to find the filter class: "+className); 166 } 167 catch( InstantiationException e ) 168 { 169 log.error("Cannot create filter class: "+className); 170 } 171 catch( IllegalAccessException e ) 172 { 173 log.error("You are not allowed to access class: "+className); 174 } 175 catch( ClassCastException e ) 176 { 177 log.error("Suggested class is not a PageFilter: "+className); 178 } 179 catch( FilterException e ) 180 { 181 log.error("Filter "+className+" failed to initialize itself.", e); 182 } 183 } 184 185 186 /** 187 * Initializes the filters from an XML file. 188 * 189 * @param props The list of properties. Typically jspwiki.properties 190 * @throws WikiException If something goes wrong. 191 */ 192 protected void initialize( Properties props ) throws WikiException { 193 InputStream xmlStream = null; 194 String xmlFile = props.getProperty( PROP_FILTERXML ) ; 195 196 try { 197 registerFilters(); 198 199 if( m_engine.getServletContext() != null ) { 200 log.debug( "Attempting to locate " + DEFAULT_XMLFILE + " from servlet context." ); 201 if( xmlFile == null ) { 202 xmlStream = m_engine.getServletContext().getResourceAsStream( DEFAULT_XMLFILE ); 203 } else { 204 xmlStream = m_engine.getServletContext().getResourceAsStream( xmlFile ); 205 } 206 } 207 208 if( xmlStream == null ) { 209 // just a fallback element to the old behaviour prior to 2.5.8 210 log.debug( "Attempting to locate filters.xml from class path." ); 211 212 if( xmlFile == null ) { 213 xmlStream = getClass().getResourceAsStream( "/filters.xml" ); 214 } else { 215 xmlStream = getClass().getResourceAsStream( xmlFile ); 216 } 217 } 218 219 if( (xmlStream == null) && (xmlFile != null) ) { 220 log.debug("Attempting to load property file "+xmlFile); 221 xmlStream = new FileInputStream( new File(xmlFile) ); 222 } 223 224 if( xmlStream == null ) { 225 log.info( "Cannot find property file for filters (this is okay, expected to find it as: '" + 226 ( xmlFile == null ? DEFAULT_XMLFILE : xmlFile ) + 227 "')" ); 228 return; 229 } 230 231 parseConfigFile( xmlStream ); 232 } catch( IOException e ) { 233 log.error("Unable to read property file", e); 234 } finally { 235 try { 236 if( xmlStream != null ) { 237 xmlStream.close(); 238 } 239 } catch( final IOException ioe ) { 240 // ignore 241 } 242 } 243 } 244 245 /** 246 * Parses the XML filters configuration file. 247 * 248 * @param xmlStream stream to parse 249 */ 250 private void parseConfigFile( InputStream xmlStream ) { 251 List< Element > pageFilters = XmlUtil.parse( xmlStream, "/pagefilters/filter" ); 252 for( Iterator< Element > i = pageFilters.iterator(); i.hasNext(); ) { 253 Element f = i.next(); 254 String filterClass = f.getChildText( "class" ); 255 Properties props = new Properties(); 256 257 List< Element > params = f.getChildren( "param" ); 258 for( Iterator< Element > par = params.iterator(); par.hasNext(); ) { 259 Element p = par.next(); 260 props.setProperty( p.getChildText( "name" ), p.getChildText( "value" ) ); 261 } 262 263 initPageFilter( filterClass, props ); 264 } 265 } 266 267 268 /** 269 * Does the filtering before a translation. 270 * 271 * @param context The WikiContext 272 * @param pageData WikiMarkup data to be passed through the preTranslate chain. 273 * @throws FilterException If any of the filters throws a FilterException 274 * @return The modified WikiMarkup 275 * 276 * @see PageFilter#preTranslate(WikiContext, String) 277 */ 278 public String doPreTranslateFiltering( WikiContext context, String pageData ) 279 throws FilterException 280 { 281 fireEvent( WikiPageEvent.PRE_TRANSLATE_BEGIN, context ); 282 283 for( PageFilter f : m_pageFilters ) 284 { 285 pageData = f.preTranslate( context, pageData ); 286 } 287 288 fireEvent( WikiPageEvent.PRE_TRANSLATE_END, context ); 289 290 return pageData; 291 } 292 293 /** 294 * Does the filtering after HTML translation. 295 * 296 * @param context The WikiContext 297 * @param htmlData HTML data to be passed through the postTranslate 298 * @throws FilterException If any of the filters throws a FilterException 299 * @return The modified HTML 300 * @see PageFilter#postTranslate(WikiContext, String) 301 */ 302 public String doPostTranslateFiltering( WikiContext context, String htmlData ) 303 throws FilterException 304 { 305 fireEvent( WikiPageEvent.POST_TRANSLATE_BEGIN, context ); 306 307 for( PageFilter f : m_pageFilters ) 308 { 309 htmlData = f.postTranslate( context, htmlData ); 310 } 311 312 fireEvent( WikiPageEvent.POST_TRANSLATE_END, context ); 313 314 return htmlData; 315 } 316 317 /** 318 * Does the filtering before a save to the page repository. 319 * 320 * @param context The WikiContext 321 * @param pageData WikiMarkup data to be passed through the preSave chain. 322 * @throws FilterException If any of the filters throws a FilterException 323 * @return The modified WikiMarkup 324 * @see PageFilter#preSave(WikiContext, String) 325 */ 326 public String doPreSaveFiltering( WikiContext context, String pageData ) 327 throws FilterException 328 { 329 fireEvent( WikiPageEvent.PRE_SAVE_BEGIN, context ); 330 331 for( PageFilter f : m_pageFilters ) 332 { 333 pageData = f.preSave( context, pageData ); 334 } 335 336 fireEvent( WikiPageEvent.PRE_SAVE_END, context ); 337 338 return pageData; 339 } 340 341 /** 342 * Does the page filtering after the page has been saved. 343 * 344 * @param context The WikiContext 345 * @param pageData WikiMarkup data to be passed through the postSave chain. 346 * @throws FilterException If any of the filters throws a FilterException 347 * 348 * @see PageFilter#postSave(WikiContext, String) 349 */ 350 public void doPostSaveFiltering( WikiContext context, String pageData ) 351 throws FilterException 352 { 353 fireEvent( WikiPageEvent.POST_SAVE_BEGIN, context ); 354 355 for( PageFilter f : m_pageFilters ) 356 { 357 // log.info("POSTSAVE: "+f.toString() ); 358 f.postSave( context, pageData ); 359 } 360 361 fireEvent( WikiPageEvent.POST_SAVE_END, context ); 362 } 363 364 /** 365 * Returns the list of filters currently installed. Note that this is not 366 * a copy, but the actual list. So be careful with it. 367 * 368 * @return A List of PageFilter objects 369 */ 370 public List< PageFilter > getFilterList() 371 { 372 return m_pageFilters; 373 } 374 375 /** 376 * 377 * Notifies PageFilters to clean up their ressources. 378 * 379 */ 380 public void destroy() 381 { 382 for( PageFilter f : m_pageFilters ) 383 { 384 f.destroy( m_engine ); 385 } 386 } 387 388 // events processing ....................................................... 389 390 /** 391 * Fires a WikiPageEvent of the provided type and WikiContext. 392 * Invalid WikiPageEvent types are ignored. 393 * 394 * @see org.apache.wiki.event.WikiPageEvent 395 * @param type the WikiPageEvent type to be fired. 396 * @param context the WikiContext of the event. 397 */ 398 public void fireEvent( int type, WikiContext context ) 399 { 400 if ( WikiEventManager.isListening(this) && WikiPageEvent.isValidType(type) ) 401 { 402 WikiEventManager.fireEvent(this, 403 new WikiPageEvent(m_engine,type,context.getPage().getName()) ); 404 } 405 } 406 407 /** 408 * {@inheritDoc} 409 */ 410 @Override 411 public Collection< WikiModuleInfo > modules() { 412 return modules( m_filterClassMap.values().iterator() ); 413 } 414 415 416 /** 417 * {@inheritDoc} 418 */ 419 @Override 420 public PageFilterInfo getModuleInfo(String moduleName) { 421 return m_filterClassMap.get(moduleName); 422 } 423 424 private void registerFilters() { 425 log.info( "Registering filters" ); 426 List< Element > filters = XmlUtil.parse( PLUGIN_RESOURCE_LOCATION, "/modules/filter" ); 427 428 // 429 // Register all filters which have created a resource containing its properties. 430 // 431 // Get all resources of all plugins. 432 // 433 for( Iterator< Element > i = filters.iterator(); i.hasNext(); ) { 434 Element pluginEl = i.next(); 435 String className = pluginEl.getAttributeValue( "class" ); 436 PageFilterInfo filterInfo = PageFilterInfo.newInstance( className, pluginEl ); 437 if( filterInfo != null ) { 438 registerFilter( filterInfo ); 439 } 440 } 441 } 442 443 private void registerFilter(PageFilterInfo pluginInfo) { 444 m_filterClassMap.put( pluginInfo.getName(), pluginInfo ); 445 } 446 447 /** 448 * Stores information about the filters. 449 * 450 * @since 2.6.1 451 */ 452 private static final class PageFilterInfo extends WikiModuleInfo 453 { 454 private PageFilterInfo( String name ) 455 { 456 super(name); 457 } 458 459 protected static PageFilterInfo newInstance(String className, Element pluginEl) 460 { 461 if( className == null || className.length() == 0 ) return null; 462 PageFilterInfo info = new PageFilterInfo( className ); 463 464 info.initializeFromXML( pluginEl ); 465 return info; 466 } 467 } 468}