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