    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}