    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.ui;
    020
    021import org.apache.commons.lang3.StringUtils;
    022import org.apache.logging.log4j.LogManager;
    023import org.apache.logging.log4j.Logger;
    024import org.apache.logging.log4j.ThreadContext;
    025import org.apache.wiki.WatchDog;
    026import org.apache.wiki.api.core.Context;
    027import org.apache.wiki.api.core.Engine;
    028import org.apache.wiki.event.WikiEventManager;
    029import org.apache.wiki.event.WikiPageEvent;
    030import org.apache.wiki.url.URLConstructor;
    031import org.apache.wiki.util.TextUtil;
    032
    033import jakarta.servlet.FilterChain;
    034import jakarta.servlet.FilterConfig;
    035import jakarta.servlet.ServletException;
    036import jakarta.servlet.ServletOutputStream;
    037import jakarta.servlet.ServletRequest;
    038import jakarta.servlet.ServletResponse;
    039import jakarta.servlet.WriteListener;
    040import jakarta.servlet.http.HttpServletRequest;
    041import jakarta.servlet.http.HttpServletResponse;
    042import jakarta.servlet.http.HttpServletResponseWrapper;
    043import java.io.ByteArrayOutputStream;
    044import java.io.IOException;
    045import java.io.OutputStreamWriter;
    046import java.io.PrintWriter;
    047import java.io.UnsupportedEncodingException;
    048
    049
    050/**
    051 * This filter goes through the generated page response prior and places requested resources at the appropriate inclusion markers.
    052 * This is done to let dynamic content (e.g. plugins, editors) include custom resources, even after the HTML head section is
    053 * in fact built. This filter is typically the last filter to execute, and it <em>must</em> run after servlet or JSP code that performs
    054 * redirections or sends error codes (such as access control methods).
    055 * <p>
    056 * Inclusion markers are placed by the IncludeResourcesTag; the default content templates (see .../templates/default/commonheader.jsp)
    057 * are configured to do this. As an example, a JavaScript resource marker is added like this:
    058 * <pre>
    059 * &lt;wiki:IncludeResources type="script"/&gt;
    060 * </pre>
    061 * Any code that requires special resources must register a resource request with the TemplateManager. For example:
    062 * <pre>
    063 * &lt;wiki:RequestResource type="script" path="scripts/custom.js" /&gt;
    064 * </pre>
    065 * or programmatically,
    066 * <pre>
    067 * TemplateManager.addResourceRequest( context, TemplateManager.RESOURCE_SCRIPT, "scripts/customresource.js" );
    068 * </pre>
    069 *
    070 * @see TemplateManager
    071 * @see org.apache.wiki.tags.RequestResourceTag
    072 */
    073public class WikiJSPFilter extends WikiServletFilter {
    074
    075    private static final Logger LOG = LogManager.getLogger( WikiJSPFilter.class );
    076    private String m_wiki_encoding;
    077    private boolean useEncoding;
    078
    079    /** {@inheritDoc} */
    080    @Override
    081    public void init( final FilterConfig config ) throws ServletException {
    082        super.init( config );
    083        m_wiki_encoding = m_engine.getWikiProperties().getProperty( Engine.PROP_ENCODING );
    084
    085        useEncoding = !Boolean.parseBoolean( m_engine.getWikiProperties().getProperty( Engine.PROP_NO_FILTER_ENCODING, "false" ).trim() );
    086    }
    087
    088    @Override
    089    public void doFilter( final ServletRequest  request, final ServletResponse response, final FilterChain chain ) throws ServletException, IOException {
    090        final WatchDog w = WatchDog.getCurrentWatchDog( m_engine );
    091        try {
    092            ThreadContext.push( m_engine.getApplicationName() + ":" + ( ( HttpServletRequest )request ).getRequestURI() );
    093            w.enterState("Filtering for URL "+((HttpServletRequest)request).getRequestURI(), 90 );
    094            final HttpServletResponseWrapper responseWrapper = new JSPWikiServletResponseWrapper( ( HttpServletResponse )response, m_wiki_encoding, useEncoding );
    095            request.setCharacterEncoding( m_engine.getContentEncoding().displayName() );
    096
    097            // fire PAGE_REQUESTED event
    098            final String pagename = URLConstructor.parsePageFromURL( ( HttpServletRequest )request, m_engine.getContentEncoding() );
    099            fireEvent( WikiPageEvent.PAGE_REQUESTED, pagename != null ? pagename : m_engine.getFrontPage() );
    100            super.doFilter( request, responseWrapper, chain );
    101
    102            // The response is now complete. Let's replace the markers now.
    103
    104            // WikiContext is only available after doFilter! (That is after interpreting the jsp)
    105
    106            try {
    107                w.enterState( "Delivering response", 30 );
    108                final Context wikiContext = getWikiContext( request );
    109                final String r = filter( wikiContext, responseWrapper );
    110
    111                if( useEncoding ) {
    112                    final OutputStreamWriter out = new OutputStreamWriter( response.getOutputStream(), response.getCharacterEncoding() );
    113                    out.write( r );
    114                    out.flush();
    115                    out.close();
    116                } else {
    117                    response.getWriter().write(r);
    118                }
    119
    120                // Clean up the UI messages and loggers
    121                if( wikiContext != null ) {
    122                    wikiContext.getWikiSession().clearMessages();
    123                }
    124
    125                // fire PAGE_DELIVERED event
    126                fireEvent( WikiPageEvent.PAGE_DELIVERED, pagename );
    127
    128            } finally {
    129                w.exitState();
    130            }
    131        } finally {
    132            w.exitState();
    133            ThreadContext.pop();
    134            ThreadContext.remove( m_engine.getApplicationName() + ":" + ( ( HttpServletRequest )request ).getRequestURI() );
    135        }
    136    }
    137
    138    /**
    139     * Goes through all types and writes the appropriate response.
    140     *
    141     * @param wikiContext The usual processing context
    142     * @param response The source string
    143     * @return The modified string with all the insertions in place.
    144     */
    145    private String filter( final Context wikiContext, final HttpServletResponse response ) {
    146        String string = response.toString();
    147
    148        if( wikiContext != null ) {
    149            final String[] resourceTypes = TemplateManager.getResourceTypes( wikiContext );
    150            for( final String resourceType : resourceTypes ) {
    151                string = insertResources( wikiContext, string, resourceType );
    152            }
    153
    154            //  Add HTTP header Resource Requests
    155            final String[] headers = TemplateManager.getResourceRequests( wikiContext, TemplateManager.RESOURCE_HTTPHEADER );
    156
    157            for( final String header : headers ) {
    158                String key = header;
    159                String value = "";
    160                final int split = header.indexOf( ':' );
    161                if( split > 0 && split < header.length() - 1 ) {
    162                    key = header.substring( 0, split );
    163                    value = header.substring( split + 1 );
    164                }
    165
    166                response.addHeader( key.trim(), value.trim() );
    167            }
    168        }
    169
    170        return string;
    171    }
    172
    173    /**
    174     *  Inserts whatever resources were requested by any plugins or other components for this particular type.
    175     *
    176     *  @param wikiContext The usual processing context
    177     *  @param string The source string
    178     *  @param type Type identifier for insertion
    179     *  @return The filtered string.
    180     */
    181    private String insertResources( final Context wikiContext, final String string, final String type ) {
    182        if( wikiContext == null ) {
    183            return string;
    184        }
    185
    186        final String marker = TemplateManager.getMarker( wikiContext, type );
    187        final int idx = string.indexOf( marker );
    188        if( idx == -1 ) {
    189            return string;
    190        }
    191
    192        LOG.debug("...Inserting...");
    193
    194        final String[] resources = TemplateManager.getResourceRequests( wikiContext, type );
    195        final StringBuilder concat = new StringBuilder( resources.length * 40 );
    196
    197        for( final String resource : resources ) {
    198            LOG.debug( "...:::" + resource );
    199            concat.append( resource );
    200        }
    201
    202        return TextUtil.replaceString( string, idx, idx + marker.length(), concat.toString() );
    203    }
    204
    205    /**
    206     *  Simple response wrapper that just allows us to gobble through the entire
    207     *  response before it's output.
    208     */
    209    private static class JSPWikiServletResponseWrapper extends HttpServletResponseWrapper {
    210
    211        final ByteArrayOutputStream m_output;
    212        private final ByteArrayServletOutputStream m_servletOut;
    213        private final PrintWriter m_writer;
    214        private final HttpServletResponse m_response;
    215        private final boolean useEncoding;
    216
    217        /** How large the initial buffer should be.  This should be tuned to achieve a balance in speed and memory consumption. */
    218        private static final int INIT_BUFFER_SIZE = 0x8000;
    219
    220        public JSPWikiServletResponseWrapper( final HttpServletResponse r, final String wikiEncoding, final boolean useEncoding ) throws UnsupportedEncodingException {
    221            super( r );
    222            m_output = new ByteArrayOutputStream( INIT_BUFFER_SIZE );
    223            m_servletOut = new ByteArrayServletOutputStream( m_output );
    224            m_writer = new PrintWriter( new OutputStreamWriter( m_servletOut, wikiEncoding ), true );
    225            this.useEncoding = useEncoding;
    226
    227            m_response = r;
    228        }
    229
    230        /** Returns a writer for output; this wraps the internal buffer into a PrintWriter. */
    231        @Override
    232        public PrintWriter getWriter() {
    233            return m_writer;
    234        }
    235
    236        @Override
    237        public ServletOutputStream getOutputStream() {
    238            return m_servletOut;
    239        }
    240
    241        @Override
    242        public void flushBuffer() throws IOException {
    243            m_writer.flush();
    244            super.flushBuffer();
    245        }
    246
    247        class ByteArrayServletOutputStream extends ServletOutputStream {
    248
    249            final ByteArrayOutputStream m_buffer;
    250
    251            public ByteArrayServletOutputStream( final ByteArrayOutputStream byteArrayOutputStream ) {
    252                super();
    253                m_buffer = byteArrayOutputStream;
    254            }
    255
    256            //
    257            /**{@inheritDoc} */
    258            @Override
    259            public void write( final int aInt ) {
    260                m_buffer.write( aInt );
    261            }
    262
    263            /**{@inheritDoc} */
    264            @Override
    265            public boolean isReady() {
    266                return false;
    267            }
    268
    269            /**{@inheritDoc} */
    270            @Override
    271            public void setWriteListener( final WriteListener writeListener ) {
    272            }
    273
    274        }
    275
    276        /** Returns whatever was written so far into the Writer. */
    277        @Override
    278        public String toString() {
    279            try {
    280                flushBuffer();
    281            } catch( final IOException e ) {
    282                LOG.error( e );
    283                return StringUtils.EMPTY;
    284            }
    285
    286            try {
    287                if( useEncoding ) {
    288                    return m_output.toString( m_response.getCharacterEncoding() );
    289                }
    290
    291                return m_output.toString();
    292            } catch( final UnsupportedEncodingException e ) {
    293                LOG.error( e );
    294                return StringUtils.EMPTY;
    295             }
    296        }
    297
    298    }
    299
    300    // events processing .......................................................
    301
    302    /**
    303     *  Fires a WikiPageEvent of the provided type and page name
    304     *  to all registered listeners of the current Engine.
    305     *
    306     * @see org.apache.wiki.event.WikiPageEvent
    307     * @param type       the event type to be fired
    308     * @param pagename   the wiki page name as a String
    309     */
    310    protected final void fireEvent( final int type, final String pagename ) {
    311        if( WikiEventManager.isListening( m_engine ) ) {
    312            WikiEventManager.fireEvent( m_engine, new WikiPageEvent( m_engine, type, pagename ));
    313        }
    314    }
    315
    316}