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