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 '{}' not compatible with this version of JSPWiki", info.getName() );
133                return;
134            }
135
136            final int priority = 0;
137            final PageFilter filter = ClassUtil.buildInstance( "org.apache.wiki.filters", className );
138            filter.initialize( m_engine, props );
139
140            addPageFilter( filter, priority );
141            log.info( "Added page filter {} with priority {}", filter.getClass().getName(), priority );
142        } catch( final ReflectiveOperationException e ) {
143            log.error( "Unable to instantiate PageFilter: {}", className );
144        } catch( final FilterException e ) {
145            log.error( "Filter {} failed to initialize itself.", className, e );
146        }
147    }
148
149
150    /**
151     *  Initializes the filters from an XML file.
152     *
153     *  @param props The list of properties. Typically, jspwiki.properties
154     *  @throws WikiException If something goes wrong.
155     */
156    protected void initialize( final Properties props ) throws WikiException {
157        InputStream xmlStream = null;
158        final String xmlFile = props.getProperty( PROP_FILTERXML ) ;
159
160        try {
161            registerFilters();
162
163            if( m_engine.getServletContext() != null ) {
164                log.debug( "Attempting to locate " + DEFAULT_XMLFILE + " from servlet context." );
165                if( xmlFile == null ) {
166                    xmlStream = m_engine.getServletContext().getResourceAsStream( DEFAULT_XMLFILE );
167                } else {
168                    xmlStream = m_engine.getServletContext().getResourceAsStream( xmlFile );
169                }
170            }
171
172            if( xmlStream == null ) {
173                // just a fallback element to the old behaviour prior to 2.5.8
174                log.debug( "Attempting to locate filters.xml from class path." );
175
176                if( xmlFile == null ) {
177                    xmlStream = getClass().getResourceAsStream( "/filters.xml" );
178                } else {
179                    xmlStream = getClass().getResourceAsStream( xmlFile );
180                }
181            }
182
183            if( (xmlStream == null) && (xmlFile != null) ) {
184                log.debug("Attempting to load property file "+xmlFile);
185                xmlStream = Files.newInputStream( new File(xmlFile).toPath() );
186            }
187
188            if( xmlStream == null ) {
189                log.info( "Cannot find property file for filters (this is okay, expected to find it as: '" + DEFAULT_XMLFILE + "')" );
190                return;
191            }
192
193            parseConfigFile( xmlStream );
194        } catch( final IOException e ) {
195            log.error("Unable to read property file", e);
196        } finally {
197            try {
198                if( xmlStream != null ) {
199                    xmlStream.close();
200                }
201            } catch( final IOException ioe ) {
202                // ignore
203            }
204        }
205    }
206
207    /**
208     *  Parses the XML filters configuration file.
209     *
210     * @param xmlStream stream to parse
211     */
212    private void parseConfigFile( final InputStream xmlStream ) {
213        final List< Element > pageFilters = XmlUtil.parse( xmlStream, "/pagefilters/filter" );
214        for( final Element f : pageFilters ) {
215            final String filterClass = f.getChildText( "class" );
216            final Properties props = new Properties();
217            final List<Element> params = f.getChildren( "param" );
218            for( final Element p : params ) {
219                props.setProperty( p.getChildText( "name" ), p.getChildText( "value" ) );
220            }
221
222            initPageFilter( filterClass, props );
223        }
224    }
225
226
227    /**
228     *  Does the filtering before a translation.
229     *
230     *  @param context The WikiContext
231     *  @param pageData WikiMarkup data to be passed through the preTranslate chain.
232     *  @throws FilterException If any of the filters throws a FilterException
233     *  @return The modified WikiMarkup
234     *
235     *  @see PageFilter#preTranslate(Context, String)
236     */
237    @Override
238    public String doPreTranslateFiltering( final Context context, String pageData ) throws FilterException {
239        fireEvent( WikiPageEvent.PRE_TRANSLATE_BEGIN, context );
240        for( final PageFilter f : m_pageFilters ) {
241            pageData = f.preTranslate( context, pageData );
242        }
243
244        fireEvent( WikiPageEvent.PRE_TRANSLATE_END, context );
245
246        return pageData;
247    }
248
249    /**
250     *  Does the filtering after HTML translation.
251     *
252     *  @param context The WikiContext
253     *  @param htmlData HTML data to be passed through the postTranslate
254     *  @throws FilterException If any of the filters throws a FilterException
255     *  @return The modified HTML
256     *  @see PageFilter#postTranslate(Context, String)
257     */
258    @Override
259    public String doPostTranslateFiltering( final Context context, String htmlData ) throws FilterException {
260        fireEvent( WikiPageEvent.POST_TRANSLATE_BEGIN, context );
261        for( final PageFilter f : m_pageFilters ) {
262            htmlData = f.postTranslate( context, htmlData );
263        }
264
265        fireEvent( WikiPageEvent.POST_TRANSLATE_END, context );
266
267        return htmlData;
268    }
269
270    /**
271     *  Does the filtering before a save to the page repository.
272     *
273     *  @param context The WikiContext
274     *  @param pageData WikiMarkup data to be passed through the preSave chain.
275     *  @throws FilterException If any of the filters throws a FilterException
276     *  @return The modified WikiMarkup
277     *  @see PageFilter#preSave(Context, String)
278     */
279    @Override
280    public String doPreSaveFiltering( final Context context, String pageData ) throws FilterException {
281        fireEvent( WikiPageEvent.PRE_SAVE_BEGIN, context );
282        for( final PageFilter f : m_pageFilters ) {
283            pageData = f.preSave( context, pageData );
284        }
285
286        fireEvent( WikiPageEvent.PRE_SAVE_END, context );
287
288        return pageData;
289    }
290
291    /**
292     *  Does the page filtering after the page has been saved.
293     *
294     *  @param context The WikiContext
295     *  @param pageData WikiMarkup data to be passed through the postSave chain.
296     *  @throws FilterException If any of the filters throws a FilterException
297     *
298     *  @see PageFilter#postSave(Context, String)
299     */
300    @Override
301    public void doPostSaveFiltering( final Context context, final String pageData ) throws FilterException {
302        fireEvent( WikiPageEvent.POST_SAVE_BEGIN, context );
303        for( final PageFilter f : m_pageFilters ) {
304            // log.info("POSTSAVE: "+f.toString() );
305            f.postSave( context, pageData );
306        }
307
308        fireEvent( WikiPageEvent.POST_SAVE_END, context );
309    }
310
311    /**
312     *  Returns the list of filters currently installed.  Note that this is not
313     *  a copy, but the actual list.  So be careful with it.
314     *
315     *  @return A List of PageFilter objects
316     */
317    @Override
318    public List< PageFilter > getFilterList()
319    {
320        return m_pageFilters;
321    }
322
323    /**
324     *
325     * Notifies PageFilters to clean up their ressources.
326     *
327     */
328    @Override
329    public void destroy() {
330        for( final PageFilter f : m_pageFilters ) {
331            f.destroy( m_engine );
332        }
333    }
334
335    // events processing .......................................................
336
337    /**
338     *  Fires a WikiPageEvent of the provided type and WikiContext. Invalid WikiPageEvent types are ignored.
339     *
340     * @see org.apache.wiki.event.WikiPageEvent
341     * @param type      the WikiPageEvent type to be fired.
342     * @param context   the WikiContext of the event.
343     */
344    public void fireEvent( final int type, final Context context ) {
345        if( WikiEventManager.isListening(this ) && WikiPageEvent.isValidType( type ) )  {
346            WikiEventManager.fireEvent(this, new WikiPageEvent( m_engine, type, context.getPage().getName() ) );
347        }
348    }
349
350    /**
351     *  {@inheritDoc}
352     */
353    @Override
354    public Collection< WikiModuleInfo > modules() {
355        return modules( m_filterClassMap.values().iterator() );
356    }
357
358    /**
359     *  {@inheritDoc}
360     */
361    @Override
362    public PageFilterInfo getModuleInfo( final String moduleName ) {
363        return m_filterClassMap.get(moduleName);
364    }
365
366    private void registerFilters() {
367        log.info( "Registering filters" );
368        final List< Element > filters = XmlUtil.parse( PLUGIN_RESOURCE_LOCATION, "/modules/filter" );
369
370        //
371        // Register all filters which have created a resource containing its properties.
372        //
373        // Get all resources of all plugins.
374        //
375        for( final Element pluginEl : filters ) {
376            final String className = pluginEl.getAttributeValue( "class" );
377            final PageFilterInfo filterInfo = PageFilterInfo.newInstance( className, pluginEl );
378            if( filterInfo != null ) {
379                registerFilter( filterInfo );
380            }
381        }
382    }
383
384    private void registerFilter( final PageFilterInfo pluginInfo ) {
385        m_filterClassMap.put( pluginInfo.getName(), pluginInfo );
386    }
387
388    /**
389     *  Stores information about the filters.
390     *
391     *  @since 2.6.1
392     */
393    private static final class PageFilterInfo extends WikiModuleInfo {
394        private PageFilterInfo( final String name ) {
395            super( name );
396        }
397
398        static PageFilterInfo newInstance( final String className, final Element pluginEl ) {
399            if( className == null || className.isEmpty() ) {
400                return null;
401            }
402            final PageFilterInfo info = new PageFilterInfo( className );
403
404            info.initializeFromXML( pluginEl );
405            return info;
406        }
407    }
408
409}