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