/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.ssi;


import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Serial;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jakarta.servlet.FilterChain;
import jakarta.servlet.GenericFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * Filter to process SSI requests within a webpage. Mapped to a content types from within web.xml.
 *
 * @author David Becker
 *
 * @see org.apache.catalina.ssi.SSIServlet
 */
public class SSIFilter extends GenericFilter {
    @Serial
    private static final long serialVersionUID = 1L;
    /** Debug level for this servlet. */
    protected int debug = 0;
    /** Expiration time in seconds for the doc. */
    protected Long expires = null;
    /** virtual path can be webapp-relative */
    protected boolean isVirtualWebappRelative = false;
    /** regex pattern to match when evaluating content types */
    protected Pattern contentTypeRegEx = null;
    /** default pattern for ssi filter content type matching */
    protected final Pattern shtmlRegEx = Pattern.compile("text/x-server-parsed-html(;.*)?");
    /** Allow exec (normally blocked for security) */
    protected boolean allowExec = false;


    @Override
    public void init() throws ServletException {
        if (getInitParameter("debug") != null) {
            debug = Integer.parseInt(getInitParameter("debug"));
        }

        if (getInitParameter("contentType") != null) {
            contentTypeRegEx = Pattern.compile(getInitParameter("contentType"));
        } else {
            contentTypeRegEx = shtmlRegEx;
        }

        isVirtualWebappRelative = Boolean.parseBoolean(getInitParameter("isVirtualWebappRelative"));

        if (getInitParameter("expires") != null) {
            expires = Long.valueOf(getInitParameter("expires"));
        }

        allowExec = Boolean.parseBoolean(getInitParameter("allowExec"));

        if (debug > 0) {
            getServletContext().log("SSIFilter.init() SSI invoker started with 'debug'=" + debug);
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // cast once
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        // setup to capture output
        ByteArrayServletOutputStream basos = new ByteArrayServletOutputStream();
        ResponseIncludeWrapper responseIncludeWrapper = new ResponseIncludeWrapper(res, basos);

        // process remainder of filter chain
        chain.doFilter(req, responseIncludeWrapper);

        // we can't assume the chain flushed its output
        responseIncludeWrapper.flushOutputStreamOrWriter();
        byte[] bytes = basos.toByteArray();

        // get content type
        String contentType = responseIncludeWrapper.getContentType();

        // is this an allowed type for SSI processing?
        if (contentType != null && contentTypeRegEx.matcher(contentType).matches()) {
            String encoding = res.getCharacterEncoding();

            // set up SSI processing
            SSIExternalResolver ssiExternalResolver = new SSIServletExternalResolver(getServletContext(), req, res,
                    isVirtualWebappRelative, debug, encoding);
            SSIProcessor ssiProcessor = new SSIProcessor(ssiExternalResolver, debug, allowExec);

            // prepare readers/writers
            Reader reader = new InputStreamReader(new ByteArrayInputStream(bytes), encoding);
            ByteArrayOutputStream ssiout = new ByteArrayOutputStream();
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(ssiout, encoding));

            // do SSI processing
            long lastModified = ssiProcessor.process(reader, responseIncludeWrapper.getLastModified(), writer);

            // set output bytes
            writer.flush();
            bytes = ssiout.toByteArray();

            // override headers
            if (expires != null) {
                res.setDateHeader("expires", (new java.util.Date()).getTime() + expires.longValue() * 1000);
            }
            if (lastModified > 0) {
                res.setDateHeader("last-modified", lastModified);
            }
            res.setContentLength(bytes.length);

            Matcher shtmlMatcher = shtmlRegEx.matcher(responseIncludeWrapper.getContentType());
            if (shtmlMatcher.matches()) {
                // Convert SHTML mime type to ordinary HTML mime type but preserve
                // encoding, if any.
                String enc = shtmlMatcher.group(1);
                res.setContentType("text/html" + ((enc != null) ? enc : ""));
            }
        }

        // write output
        OutputStream out = null;
        try {
            out = res.getOutputStream();
        } catch (IllegalStateException e) {
            // Ignore, will try to use a writer
        }
        if (out == null) {
            res.getWriter().write(new String(bytes));
        } else {
            out.write(bytes);
        }
    }
}
