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