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