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