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