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