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