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;
032
033import org.apache.commons.io.IOUtils;
034import org.apache.log4j.Logger;
035import org.apache.wiki.WikiContext;
036import org.apache.wiki.WikiEngine;
037import org.apache.wiki.api.engine.FilterManager;
038import org.apache.wiki.api.exceptions.FilterException;
039import org.apache.wiki.api.exceptions.WikiException;
040import org.apache.wiki.api.filters.PageFilter;
041import org.apache.wiki.event.WikiEventManager;
042import org.apache.wiki.event.WikiPageEvent;
043import org.apache.wiki.modules.ModuleManager;
044import org.apache.wiki.modules.WikiModuleInfo;
045import org.apache.wiki.util.ClassUtil;
046import org.apache.wiki.util.PriorityList;
047import org.apache.wiki.util.XmlUtil;
048import 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 &lt;filter> -sections define the filters.  For more information, please see
094 *  the PageFilterConfiguration page in the JSPWiki distribution.
095 */
096public 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    @Override
407    public Collection modules()
408    {
409        ArrayList< PageFilter > modules = new ArrayList< PageFilter >();
410        
411        modules.addAll( m_pageFilters );
412        
413        return modules;
414    }
415    
416    
417    /**
418     *  {@inheritDoc}
419     */
420    @Override
421    public PageFilterInfo getModuleInfo(String moduleName) {
422        return m_filterClassMap.get(moduleName);
423    }
424
425    private void registerFilters() {
426        log.info( "Registering filters" );
427        List< Element > filters = XmlUtil.parse( PLUGIN_RESOURCE_LOCATION, "/modules/filter" );
428
429        //
430        // Register all filters which have created a resource containing its properties.
431        //
432        // Get all resources of all plugins.
433        //
434        for( Iterator< Element > i = filters.iterator(); i.hasNext(); ) {
435            Element pluginEl = i.next();
436            String className = pluginEl.getAttributeValue( "class" );
437            PageFilterInfo filterInfo = PageFilterInfo.newInstance( className, pluginEl );
438            if( filterInfo != null ) {
439                registerFilter( filterInfo );
440            }
441        }
442    }
443
444    private void registerFilter(PageFilterInfo pluginInfo) {
445        m_filterClassMap.put( pluginInfo.getName(), pluginInfo );
446    }
447
448    /**
449     *  Stores information about the filters.
450     * 
451     *  @since 2.6.1
452     */
453    private static final class PageFilterInfo extends WikiModuleInfo
454    {
455        private PageFilterInfo( String name )
456        {
457            super(name);
458        }
459        
460        protected static PageFilterInfo newInstance(String className, Element pluginEl)
461        {
462            if( className == null || className.length() == 0 ) return null;
463            PageFilterInfo info = new PageFilterInfo( className );
464
465            info.initializeFromXML( pluginEl );
466            return info;        
467        }
468    }
469}