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