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