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