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 '{}' not compatible with this version of JSPWiki", info.getName() ); 133 return; 134 } 135 136 final int priority = 0; 137 final PageFilter filter = ClassUtil.buildInstance( "org.apache.wiki.filters", className ); 138 filter.initialize( m_engine, props ); 139 140 addPageFilter( filter, priority ); 141 log.info( "Added page filter {} with priority {}", filter.getClass().getName(), priority ); 142 } catch( final ReflectiveOperationException e ) { 143 log.error( "Unable to instantiate PageFilter: {}", className ); 144 } catch( final FilterException e ) { 145 log.error( "Filter {} failed to initialize itself.", className, e ); 146 } 147 } 148 149 150 /** 151 * Initializes the filters from an XML file. 152 * 153 * @param props The list of properties. Typically, jspwiki.properties 154 * @throws WikiException If something goes wrong. 155 */ 156 protected void initialize( final Properties props ) throws WikiException { 157 InputStream xmlStream = null; 158 final String xmlFile = props.getProperty( PROP_FILTERXML ) ; 159 160 try { 161 registerFilters(); 162 163 if( m_engine.getServletContext() != null ) { 164 log.debug( "Attempting to locate " + DEFAULT_XMLFILE + " from servlet context." ); 165 if( xmlFile == null ) { 166 xmlStream = m_engine.getServletContext().getResourceAsStream( DEFAULT_XMLFILE ); 167 } else { 168 xmlStream = m_engine.getServletContext().getResourceAsStream( xmlFile ); 169 } 170 } 171 172 if( xmlStream == null ) { 173 // just a fallback element to the old behaviour prior to 2.5.8 174 log.debug( "Attempting to locate filters.xml from class path." ); 175 176 if( xmlFile == null ) { 177 xmlStream = getClass().getResourceAsStream( "/filters.xml" ); 178 } else { 179 xmlStream = getClass().getResourceAsStream( xmlFile ); 180 } 181 } 182 183 if( (xmlStream == null) && (xmlFile != null) ) { 184 log.debug("Attempting to load property file "+xmlFile); 185 xmlStream = Files.newInputStream( new File(xmlFile).toPath() ); 186 } 187 188 if( xmlStream == null ) { 189 log.info( "Cannot find property file for filters (this is okay, expected to find it as: '" + DEFAULT_XMLFILE + "')" ); 190 return; 191 } 192 193 parseConfigFile( xmlStream ); 194 } catch( final IOException e ) { 195 log.error("Unable to read property file", e); 196 } finally { 197 try { 198 if( xmlStream != null ) { 199 xmlStream.close(); 200 } 201 } catch( final IOException ioe ) { 202 // ignore 203 } 204 } 205 } 206 207 /** 208 * Parses the XML filters configuration file. 209 * 210 * @param xmlStream stream to parse 211 */ 212 private void parseConfigFile( final InputStream xmlStream ) { 213 final List< Element > pageFilters = XmlUtil.parse( xmlStream, "/pagefilters/filter" ); 214 for( final Element f : pageFilters ) { 215 final String filterClass = f.getChildText( "class" ); 216 final Properties props = new Properties(); 217 final List<Element> params = f.getChildren( "param" ); 218 for( final Element p : params ) { 219 props.setProperty( p.getChildText( "name" ), p.getChildText( "value" ) ); 220 } 221 222 initPageFilter( filterClass, props ); 223 } 224 } 225 226 227 /** 228 * Does the filtering before a translation. 229 * 230 * @param context The WikiContext 231 * @param pageData WikiMarkup data to be passed through the preTranslate chain. 232 * @throws FilterException If any of the filters throws a FilterException 233 * @return The modified WikiMarkup 234 * 235 * @see PageFilter#preTranslate(Context, String) 236 */ 237 @Override 238 public String doPreTranslateFiltering( final Context context, String pageData ) throws FilterException { 239 fireEvent( WikiPageEvent.PRE_TRANSLATE_BEGIN, context ); 240 for( final PageFilter f : m_pageFilters ) { 241 pageData = f.preTranslate( context, pageData ); 242 } 243 244 fireEvent( WikiPageEvent.PRE_TRANSLATE_END, context ); 245 246 return pageData; 247 } 248 249 /** 250 * Does the filtering after HTML translation. 251 * 252 * @param context The WikiContext 253 * @param htmlData HTML data to be passed through the postTranslate 254 * @throws FilterException If any of the filters throws a FilterException 255 * @return The modified HTML 256 * @see PageFilter#postTranslate(Context, String) 257 */ 258 @Override 259 public String doPostTranslateFiltering( final Context context, String htmlData ) throws FilterException { 260 fireEvent( WikiPageEvent.POST_TRANSLATE_BEGIN, context ); 261 for( final PageFilter f : m_pageFilters ) { 262 htmlData = f.postTranslate( context, htmlData ); 263 } 264 265 fireEvent( WikiPageEvent.POST_TRANSLATE_END, context ); 266 267 return htmlData; 268 } 269 270 /** 271 * Does the filtering before a save to the page repository. 272 * 273 * @param context The WikiContext 274 * @param pageData WikiMarkup data to be passed through the preSave chain. 275 * @throws FilterException If any of the filters throws a FilterException 276 * @return The modified WikiMarkup 277 * @see PageFilter#preSave(Context, String) 278 */ 279 @Override 280 public String doPreSaveFiltering( final Context context, String pageData ) throws FilterException { 281 fireEvent( WikiPageEvent.PRE_SAVE_BEGIN, context ); 282 for( final PageFilter f : m_pageFilters ) { 283 pageData = f.preSave( context, pageData ); 284 } 285 286 fireEvent( WikiPageEvent.PRE_SAVE_END, context ); 287 288 return pageData; 289 } 290 291 /** 292 * Does the page filtering after the page has been saved. 293 * 294 * @param context The WikiContext 295 * @param pageData WikiMarkup data to be passed through the postSave chain. 296 * @throws FilterException If any of the filters throws a FilterException 297 * 298 * @see PageFilter#postSave(Context, String) 299 */ 300 @Override 301 public void doPostSaveFiltering( final Context context, final String pageData ) throws FilterException { 302 fireEvent( WikiPageEvent.POST_SAVE_BEGIN, context ); 303 for( final PageFilter f : m_pageFilters ) { 304 // log.info("POSTSAVE: "+f.toString() ); 305 f.postSave( context, pageData ); 306 } 307 308 fireEvent( WikiPageEvent.POST_SAVE_END, context ); 309 } 310 311 /** 312 * Returns the list of filters currently installed. Note that this is not 313 * a copy, but the actual list. So be careful with it. 314 * 315 * @return A List of PageFilter objects 316 */ 317 @Override 318 public List< PageFilter > getFilterList() 319 { 320 return m_pageFilters; 321 } 322 323 /** 324 * 325 * Notifies PageFilters to clean up their ressources. 326 * 327 */ 328 @Override 329 public void destroy() { 330 for( final PageFilter f : m_pageFilters ) { 331 f.destroy( m_engine ); 332 } 333 } 334 335 // events processing ....................................................... 336 337 /** 338 * Fires a WikiPageEvent of the provided type and WikiContext. Invalid WikiPageEvent types are ignored. 339 * 340 * @see org.apache.wiki.event.WikiPageEvent 341 * @param type the WikiPageEvent type to be fired. 342 * @param context the WikiContext of the event. 343 */ 344 public void fireEvent( final int type, final Context context ) { 345 if( WikiEventManager.isListening(this ) && WikiPageEvent.isValidType( type ) ) { 346 WikiEventManager.fireEvent(this, new WikiPageEvent( m_engine, type, context.getPage().getName() ) ); 347 } 348 } 349 350 /** 351 * {@inheritDoc} 352 */ 353 @Override 354 public Collection< WikiModuleInfo > modules() { 355 return modules( m_filterClassMap.values().iterator() ); 356 } 357 358 /** 359 * {@inheritDoc} 360 */ 361 @Override 362 public PageFilterInfo getModuleInfo( final String moduleName ) { 363 return m_filterClassMap.get(moduleName); 364 } 365 366 private void registerFilters() { 367 log.info( "Registering filters" ); 368 final List< Element > filters = XmlUtil.parse( PLUGIN_RESOURCE_LOCATION, "/modules/filter" ); 369 370 // 371 // Register all filters which have created a resource containing its properties. 372 // 373 // Get all resources of all plugins. 374 // 375 for( final Element pluginEl : filters ) { 376 final String className = pluginEl.getAttributeValue( "class" ); 377 final PageFilterInfo filterInfo = PageFilterInfo.newInstance( className, pluginEl ); 378 if( filterInfo != null ) { 379 registerFilter( filterInfo ); 380 } 381 } 382 } 383 384 private void registerFilter( final PageFilterInfo pluginInfo ) { 385 m_filterClassMap.put( pluginInfo.getName(), pluginInfo ); 386 } 387 388 /** 389 * Stores information about the filters. 390 * 391 * @since 2.6.1 392 */ 393 private static final class PageFilterInfo extends WikiModuleInfo { 394 private PageFilterInfo( final String name ) { 395 super( name ); 396 } 397 398 static PageFilterInfo newInstance( final String className, final Element pluginEl ) { 399 if( className == null || className.isEmpty() ) { 400 return null; 401 } 402 final PageFilterInfo info = new PageFilterInfo( className ); 403 404 info.initializeFromXML( pluginEl ); 405 return info; 406 } 407 } 408 409}