001package jmri.web.servlet.help;
002
003import java.io.*;
004import java.nio.charset.StandardCharsets;
005import java.nio.file.Files;
006import java.nio.file.Paths;
007import java.util.regex.*;
008
009import javax.servlet.ServletException;
010import javax.servlet.annotation.WebServlet;
011import javax.servlet.http.HttpServlet;
012import javax.servlet.http.HttpServletRequest;
013import javax.servlet.http.HttpServletResponse;
014
015import static jmri.web.servlet.ServletUtil.APPLICATION_JAVASCRIPT;
016import static jmri.web.servlet.ServletUtil.UTF8_TEXT_HTML;
017import jmri.util.FileUtil;
018
019import org.eclipse.jetty.server.Request;
020import org.openide.util.lookup.ServiceProvider;
021
022/**
023 * Parse server side include tags on web pages
024 * @author Randall Wood     (C) 2014, 2016
025 * @author Daniel Bergqvist (C) 2021
026 * @author mstevetodd (C) 2023
027 */
028@WebServlet(name = "HelpSSIServlet",
029        urlPatterns = {
030            "/help",
031            "/plugin"
032        })
033@ServiceProvider(service = HttpServlet.class)
034public class HelpSSIServlet extends HttpServlet {
035
036    private void handleRegularFile(String fileName, HttpServletResponse response) throws IOException {
037        response.setHeader("Connection", "Keep-Alive"); // NOI18N
038        String ext = fileName.substring(fileName.lastIndexOf('.')+1).toLowerCase();
039        switch (ext) {
040            case "svg":
041                response.setContentType("image/svg");
042                break;
043            case "png":
044                response.setContentType("image/png");
045                break;
046            case "gif":
047                response.setContentType("image/gif");
048                break;
049            case "jpg":
050            case "jpeg":
051                response.setContentType("image/jpeg");
052                break;
053            case "js":
054                response.setContentType(APPLICATION_JAVASCRIPT);
055                break;
056            default:
057                response.setContentType("application/octet-stream");
058        }
059        byte[] b = new byte[1024];
060        if (fileName.startsWith("/plugin/")) {
061            String resourceName = fileName.substring("/plugin/".length());
062            try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(resourceName)) {
063                if (inputStream != null) {
064                    int byteRead;
065                    while ((byteRead = inputStream.read(b)) != -1) {
066                        response.getOutputStream().write(b, 0, byteRead);
067                    }
068                } else {
069                    String error = String.format("--- ERROR: Plugin resource \"%s\" couldn't be found", resourceName);
070                    response.getOutputStream().write(error.getBytes());
071                    log.warn(error);
072                }
073            }
074        } else {
075            try (InputStream inputStream = new FileInputStream(fileName);) {
076                int byteRead;
077                while ((byteRead = inputStream.read(b)) != -1) {
078                    response.getOutputStream().write(b, 0, byteRead);
079                }
080            }
081        }
082        response.getOutputStream().flush();
083    }
084
085    private String convertDotDotFolders(String theFileName, String path) {
086        if (theFileName.startsWith("../")) {
087            String[] paths = path.split("/");
088            int numDotDots = 0;
089            while (theFileName.startsWith("../")) {
090                theFileName = theFileName.substring(3);
091                numDotDots++;
092            }
093            if (numDotDots < paths.length) {
094                StringBuilder sb = new StringBuilder();
095                for (int i=0; i < (paths.length - numDotDots); i++) {
096                    sb.append(paths[i]).append('/');
097                }
098                theFileName = sb.toString() + theFileName;
099            } else {
100                // We have more ../ than subfolders in path
101                theFileName = '/' + theFileName;
102            }
103        }
104        return theFileName;
105    }
106
107    private String quoteBackslash(String content) {
108        // A single backslash needs to be replaced by a double backslash
109        return content.replaceAll("\\\\", "\\\\\\\\");
110    }
111
112    private String readAndParseFile(String fileName) throws IOException {
113        log.debug("readAndParseFile('{}')", fileName);
114
115        int lastSlash = fileName.lastIndexOf('/');
116        String path = lastSlash != -1 ? fileName.substring(0, lastSlash+1) : "";
117
118        if (!fileName.startsWith("/plugin/")) {
119            fileName = FileUtil.getProgramPath() + fileName;
120        }
121
122        String content;
123        try {
124            if (fileName.startsWith("/plugin/")) {
125                String resourceName = fileName.substring("/plugin/".length());
126                try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourceName)) {
127                    if (is != null) {
128                        try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
129
130                            StringBuilder sb = new StringBuilder();
131                            String line;
132                            while ((line = reader.readLine()) != null) {
133                                sb.append(line);
134                            }
135                            content = sb.toString();
136                        }
137                    } else {
138                        content = String.format("%n<br>%nERROR: Plugin resource \"%s\" couldn't be found%n<br>%n", resourceName);
139                        log.warn("Plugin resource \"{}\" couldn't be found", resourceName);
140                    }
141                }
142            } else {
143                content = new String(Files.readAllBytes(Paths.get(fileName)));
144            }
145        } catch (IOException ex) {
146            content = "Exception thrown: " + ex.getMessage();
147            log.warn("Cannot read file: {}", fileName, ex);
148        }
149
150        String serverSideIncludePattern = "<!--#include\\s*virtual=\"(.+?)\"\\s*-->";
151
152        Pattern pattern = Pattern.compile(serverSideIncludePattern);
153        Matcher matcher = pattern.matcher(content);
154
155        content = matcher.replaceAll((MatchResult t) -> {
156            String theFileName = t.group(1);
157            try {
158                theFileName = convertDotDotFolders(theFileName, path);
159
160                if (path.startsWith("/")) {
161                    if (theFileName.startsWith("/")) {
162                        return quoteBackslash(readAndParseFile(theFileName));
163                    } else {
164                        return quoteBackslash(readAndParseFile(path + theFileName));
165                    }
166                } else {
167                    return quoteBackslash(readAndParseFile("web/" + path + theFileName));
168                }
169            } catch (IOException ex) {
170                log.warn("Cannot include SSI: {}", theFileName, ex);
171                return "";
172            }
173        });
174        return content;
175    }
176
177    protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
178
179        String uri = request.getRequestURI();
180        if (!uri.endsWith(".shtml")) {
181            if (!(request instanceof Request)) throw new IllegalArgumentException("request is not a Request");
182            log.debug("Handling regular file: '{}'", uri);
183            String fileName = uri;
184            if (!fileName.startsWith("/plugin/")) {
185                fileName = FileUtil.getProgramPath() + uri;
186            }
187            handleRegularFile(fileName, response);
188            return;
189        }
190
191        log.debug("Handling .shtml  file: '{}'", uri);
192        String content = readAndParseFile(uri);
193
194        response.setHeader("Connection", "Keep-Alive"); // NOI18N
195        response.setContentType(UTF8_TEXT_HTML);
196        response.getWriter().write(content);
197    }
198
199// <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">
200    /**
201     * Handles the HTTP <code>GET</code> method.
202     *
203     * @param request  servlet request
204     * @param response servlet response
205     * @throws ServletException if a servlet-specific error occurs
206     * @throws IOException      if an I/O error occurs
207     */
208    @Override
209    protected void doGet(HttpServletRequest request, HttpServletResponse response)
210            throws ServletException, IOException {
211        processRequest(request, response);
212    }
213
214    /**
215     * Handles the HTTP <code>POST</code> method.
216     *
217     * @param request  servlet request
218     * @param response servlet response
219     * @throws ServletException if a servlet-specific error occurs
220     * @throws IOException      if an I/O error occurs
221     */
222    @Override
223    protected void doPost(HttpServletRequest request, HttpServletResponse response)
224            throws ServletException, IOException {
225        processRequest(request, response);
226    }
227
228    /**
229     * Returns a short description of the servlet.
230     *
231     * @return a String containing servlet description
232     */
233    @Override
234    public String getServletInfo() {
235        return "Help SSI Servlet";
236    }// </editor-fold>
237
238
239    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(HelpSSIServlet.class);
240}