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