SLING-4552 : Escape output in webconsole plugin

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1670022 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/sling/commons/log/logback/internal/SlingConfigurationPrinter.java b/src/main/java/org/apache/sling/commons/log/logback/internal/SlingConfigurationPrinter.java
index bcd3535..151def6 100644
--- a/src/main/java/org/apache/sling/commons/log/logback/internal/SlingConfigurationPrinter.java
+++ b/src/main/java/org/apache/sling/commons/log/logback/internal/SlingConfigurationPrinter.java
@@ -90,7 +90,7 @@
      * if some complex rotation logic is used where rotated file get different names
      * or get created in different directory then those files would not be
      * included
-     * 
+     *
      * @see org.apache.felix.webconsole.AttachmentProvider#getAttachments(String)
      */
     @SuppressWarnings("UnusedDeclaration")
@@ -128,6 +128,7 @@
             final File dir = file.getParentFile();
             final String baseName = file.getName();
             return dir.listFiles(new FilenameFilter() {
+                @Override
                 public boolean accept(File dir, String name) {
                     return name.startsWith(baseName);
                 }
diff --git a/src/main/java/org/apache/sling/commons/log/logback/internal/SlingLogPanel.java b/src/main/java/org/apache/sling/commons/log/logback/internal/SlingLogPanel.java
index 420994e..33fa2b8 100644
--- a/src/main/java/org/apache/sling/commons/log/logback/internal/SlingLogPanel.java
+++ b/src/main/java/org/apache/sling/commons/log/logback/internal/SlingLogPanel.java
@@ -18,6 +18,9 @@
  */
 package org.apache.sling.commons.log.logback.internal;
 
+import static org.apache.sling.commons.log.logback.internal.LogbackManager.APP_ROOT;
+import static org.apache.sling.commons.log.logback.internal.LogbackManager.RES_LOC;
+
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -39,17 +42,8 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import ch.qos.logback.classic.Level;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.classic.turbo.TurboFilter;
-import ch.qos.logback.core.Appender;
-import ch.qos.logback.core.CoreConstants;
-import ch.qos.logback.core.FileAppender;
-import ch.qos.logback.core.helpers.Transform;
-import ch.qos.logback.core.status.Status;
-import ch.qos.logback.core.util.CachingDateFormatter;
 import org.apache.sling.commons.log.logback.internal.AppenderTracker.AppenderInfo;
+import org.apache.sling.commons.log.logback.internal.ConfigSourceTracker.ConfigSourceInfo;
 import org.apache.sling.commons.log.logback.internal.LogbackManager.LoggerStateContext;
 import org.apache.sling.commons.log.logback.internal.config.ConfigurationException;
 import org.apache.sling.commons.log.logback.internal.util.SlingRollingFileAppender;
@@ -63,9 +57,16 @@
 import org.slf4j.LoggerFactory;
 import org.xml.sax.InputSource;
 
-import static org.apache.sling.commons.log.logback.internal.ConfigSourceTracker.ConfigSourceInfo;
-import static org.apache.sling.commons.log.logback.internal.LogbackManager.APP_ROOT;
-import static org.apache.sling.commons.log.logback.internal.LogbackManager.RES_LOC;
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.turbo.TurboFilter;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.CoreConstants;
+import ch.qos.logback.core.FileAppender;
+import ch.qos.logback.core.helpers.Transform;
+import ch.qos.logback.core.status.Status;
+import ch.qos.logback.core.util.CachingDateFormatter;
 
 /**
  * The <code>SlingLogPanel</code> is a Felix Web Console plugin to display the
@@ -100,7 +101,7 @@
 
     private static final org.slf4j.Logger log = LoggerFactory.getLogger(SlingLogPanel.class);
 
-    public SlingLogPanel(final LogbackManager logbackManager, BundleContext bundleContext) {
+    public SlingLogPanel(final LogbackManager logbackManager, final BundleContext bundleContext) {
         this.logbackManager = logbackManager;
         this.bundleContext = bundleContext;
         this.labelRes = '/' + APP_ROOT + '/';
@@ -108,12 +109,11 @@
     }
 
     @Override
-    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
 
         final PrintWriter pw = resp.getWriter();
 
         final String consoleAppRoot = (String) req.getAttribute("felix.webconsole.appRoot");
-        final String pluginRoot = (String) req.getAttribute("felix.webconsole.pluginRoot");
 
         final LoggerStateContext ctx = logbackManager.determineLoggerState();
         appendLoggerStatus(pw, ctx);
@@ -153,12 +153,11 @@
             internalFailure("", e);
         }
         // send the redirect back to the logpanel
-        final String consoleAppRoot = (String) req
-                .getAttribute("felix.webconsole.appRoot");
+        final String consoleAppRoot = (String) req.getAttribute("felix.webconsole.appRoot");
         resp.sendRedirect(consoleAppRoot + "/" + APP_ROOT);
     }
 
-    private void addScriptBlock(PrintWriter pw, LoggerStateContext ctx) {
+    private void addScriptBlock(final PrintWriter pw, final LoggerStateContext ctx) {
         pw.println("<script type=\"text/javascript\" src=\"" + RES_LOC + "/slinglog.js\"></script>");
         pw.println("<script type=\"text/javascript\" src=\"" + RES_LOC + "/jquery.autocomplete.min.js\"></script>");
         pw.println("<script type=\"text/javascript\" src=\"" + RES_LOC + "/prettify.js\"></script>");
@@ -184,7 +183,9 @@
         loggers.addAll(packageList);
         for (Iterator<String> loggerIt = loggers.iterator(); loggerIt.hasNext(); ) {
             String logger = loggerIt.next();
-            pw.print("'" + logger + "'");
+            pw.print("'");
+            pw.print(XmlUtil.escapeXml(logger));
+            pw.print("'");
             if (loggerIt.hasNext()) {
                 pw.print(",");
             }
@@ -200,7 +201,7 @@
                 ctx.getNumberOfLoggers(), ctx.getNumOfAppenders(), ctx.getNumOfDynamicAppenders());
     }
 
-    private void appendOsgiConfiguredLoggerData(PrintWriter pw, String consoleAppRoot) {
+    private void appendOsgiConfiguredLoggerData(final PrintWriter pw, final String consoleAppRoot) {
         pw.println("<div class='table'>");
 
         pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Logger (Configured via OSGi Config)</div>");
@@ -212,35 +213,48 @@
         pw.println("<th>Log Level</th>");
         pw.println("<th>Log File</th>");
         pw.println("<th>Logger</th>");
-        pw.println("<th width=\"20%\">" + getConfigColTitle(consoleAppRoot) + "</th>");
+        pw.print("<th width=\"20%\">");
+        pw.print(getConfigColTitle(consoleAppRoot)); // no need to escape
+        pw.println("</th>");
         pw.println("</tr>");
         pw.println("</thead>");
         pw.println("<tbody class='ui-widget-content'>");
 
         final LogConfigManager configManager = logbackManager.getLogConfigManager();
-        String rootPath = logbackManager.getRootDir();
-        boolean shortenPaths = areAllLogfilesInSameFolder(configManager.getLogWriters(), rootPath);
-        for (LogConfig logConfig : configManager.getLogConfigs()) {
-            pw.println("<tr id=\"" + logConfig.getConfigPid() + "\">");
-            pw.println("<td><span class=\"logLevels\" data-currentloglevel=\""
-                    + logConfig.getLogLevel().levelStr + "\">" + logConfig.getLogLevel().levelStr
-                    + "</span></td>");
-            pw.println("<td><span class=\"logFile\">" + getPath(logConfig.getLogWriterName(), rootPath, shortenPaths) + "</span></td>");
+        final String rootPath = logbackManager.getRootDir();
+        final boolean shortenPaths = areAllLogfilesInSameFolder(configManager.getLogWriters(), rootPath);
+        for (final LogConfig logConfig : configManager.getLogConfigs()) {
+            pw.print("<tr id=\"");
+            pw.print( XmlUtil.escapeXml(logConfig.getConfigPid()) );
+            pw.println("\">");
+            pw.print("<td><span class=\"logLevels\" data-currentloglevel=\"");
+            pw.print(logConfig.getLogLevel().levelStr);
+            pw.print("\">");
+            pw.print(logConfig.getLogLevel().levelStr);
+            pw.println("</span></td>");
+            pw.print("<td><span class=\"logFile\">");
+            pw.print( XmlUtil.escapeXml(getPath(logConfig.getLogWriterName(), rootPath, shortenPaths)));
+            pw.println("</span></td>");
 
             pw.println("<td><span class=\"loggers\">");
             String sep = "";
-            for (String cat : logConfig.getCategories()) {
-                pw.println(sep + "<span class=\"logger\">" + cat + "</span>");
+            for (final String cat : logConfig.getCategories()) {
+                pw.print(sep);
+                pw.print("<span class=\"logger\">");
+                pw.print( XmlUtil.escapeXml(cat));
+                pw.println("</span>");
                 sep = "<br />";
             }
             pw.println("</td>");
 
-            String pid = logConfig.getConfigPid();
+            final String pid = logConfig.getConfigPid();
             String url = createUrl(consoleAppRoot, "configMgr", pid, true);
             if (logConfig.getCategories().contains(Logger.ROOT_LOGGER_NAME)) {
                 url = createUrl(consoleAppRoot, "configMgr", pid, false);
             }
-            pw.println("<td>" + url + "</td>");
+            pw.print("<td>");
+            pw.print(url);
+            pw.println("</td>");
             pw.println("</tr>");
         }
 
@@ -248,15 +262,16 @@
         pw.println("<tr id=\"newlogger\">");
         pw.println("<td><span id=\"allLogLevels\" class=\"logLevels\" data-loglevels=\"");
         String sep = "";
-        for (String levelName : LEVEL_NAMES) {
-            pw.print(sep + levelName);
+        for (final String levelName : LEVEL_NAMES) {
+            pw.print(sep);
+            pw.print(XmlUtil.escapeXml(levelName));
             sep = ",";
         }
 
         pw.println("\"></span></td>");
-        pw.println("<td><span id=\"defaultLogfile\" data-defaultlogfile=\""
-                + getPath(configManager.getDefaultWriter().getFileName(), rootPath, shortenPaths)
-                + "\" class=\"logFile\"></span></td>");
+        pw.print("<td><span id=\"defaultLogfile\" data-defaultlogfile=\"");
+        pw.print( XmlUtil.escapeXml(getPath(configManager.getDefaultWriter().getFileName(), rootPath, shortenPaths)));
+        pw.println("\" class=\"logFile\"></span></td>");
         pw.println("<td><span class=\"loggers\"></span></td>");
         pw.println("<td><input type='submit' class=\"configureLink\" value='Add new Logger' /></td></tr></tfoot>");
 
@@ -264,7 +279,7 @@
         pw.println("</div>");
     }
 
-    private void appendOtherLoggerData(PrintWriter pw, LoggerStateContext ctx) {
+    private void appendOtherLoggerData(final PrintWriter pw, final LoggerStateContext ctx) {
         if (ctx.nonOSgiConfiguredLoggers.isEmpty()) {
             return;
         }
@@ -285,19 +300,25 @@
         pw.println("</thead>");
         pw.println("<tbody class='ui-widget-content'>");
 
-        for (Logger logger : ctx.nonOSgiConfiguredLoggers) {
+        for (final Logger logger : ctx.nonOSgiConfiguredLoggers) {
             pw.println("<tr>");
-            pw.println("<td>" + logger.getLevel() + "</td>");
-            pw.println("<td>" + Boolean.toString(logger.isAdditive()) + "</td>");
-            pw.println("<td>" + logger.getName() + "</td>");
+            pw.print("<td>");
+            pw.print(logger.getLevel());
+            pw.println("</td>");
+            pw.print("<td>");
+            pw.print(Boolean.toString(logger.isAdditive()));
+            pw.println("</td>");
+            pw.print("<td>");
+            pw.print( XmlUtil.escapeXml(logger.getName()));
+            pw.println("</td>");
 
             pw.println("<td>");
             pw.println("<ul>");
-            Iterator<Appender<ILoggingEvent>> itr = logger.iteratorForAppenders();
+            final Iterator<Appender<ILoggingEvent>> itr = logger.iteratorForAppenders();
             while (itr.hasNext()) {
-                Appender<ILoggingEvent> a = itr.next();
+                final Appender<ILoggingEvent> a = itr.next();
                 pw.print("<li>");
-                pw.print(getName(a));
+                pw.print(XmlUtil.escapeXml(getName(a)));
                 pw.print("</li>");
             }
             pw.println("</ul>");
@@ -310,7 +331,7 @@
         pw.println("</div>");
     }
 
-    private void addAppenderData(PrintWriter pw, String consoleAppRoot, LoggerStateContext ctx) {
+    private void addAppenderData(final PrintWriter pw, final String consoleAppRoot, final LoggerStateContext ctx) {
         pw.println("<div class='table'>");
 
         pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Appender</div>");
@@ -320,15 +341,21 @@
         pw.println("<thead class='ui-widget-header'>");
         pw.println("<tr>");
         pw.println("<th>Appender</th>");
-        pw.println("<th>" + getConfigColTitle(consoleAppRoot) + "</th>");
+        pw.print("<th>");
+        pw.print(getConfigColTitle(consoleAppRoot)); // no need to escape
+        pw.println("</th>");
         pw.println("</tr>");
         pw.println("</thead>");
         pw.println("<tbody class='ui-widget-content'>");
 
-        for (Appender<ILoggingEvent> appender : ctx.appenders.values()) {
+        for (final Appender<ILoggingEvent> appender : ctx.appenders.values()) {
             pw.println("<tr>");
-            pw.println("<td>" + getName(appender) + "</td>");
-            pw.println("<td>" + formatPid(consoleAppRoot, appender, ctx) + "</td>");
+            pw.print("<td>");
+            pw.print(XmlUtil.escapeXml(getName(appender)));
+            pw.println("</td>");
+            pw.print("<td>");
+            pw.print(formatPid(consoleAppRoot, appender, ctx));
+            pw.println("</td>");
             pw.println("</tr>");
         }
 
@@ -337,7 +364,7 @@
         pw.println("</div>");
     }
 
-    private void appendTurboFilterData(PrintWriter pw, String consoleAppRoot, LoggerStateContext ctx) {
+    private void appendTurboFilterData(final PrintWriter pw, final String consoleAppRoot, final LoggerStateContext ctx) {
         if (ctx.loggerContext.getTurboFilterList().isEmpty()) {
             return;
         }
@@ -351,16 +378,22 @@
         pw.println("<thead class='ui-widget-header'>");
         pw.println("<tr>");
         pw.println("<th>Turbo Filter</th>");
-        pw.println("<th>" + getConfigColTitle(consoleAppRoot) + "</th>");
+        pw.print("<th>");
+        pw.print(getConfigColTitle(consoleAppRoot)); // no need to escape
+        pw.println("</th>");
         pw.println("</tr>");
         pw.println("</thead>");
         pw.println("<tbody class='ui-widget-content'>");
 
 
-        for (TurboFilter tf : ctx.loggerContext.getTurboFilterList()) {
+        for (final TurboFilter tf : ctx.loggerContext.getTurboFilterList()) {
             pw.println("<tr>");
-            pw.println("<td>" + getName(tf) + "</td>");
-            pw.println("<td>" + formatPid(consoleAppRoot, tf, ctx) + "</td>");
+            pw.println("<td>");
+            pw.print(XmlUtil.escapeXml(getName(tf)));
+            pw.println("</td>");
+            pw.print("<td>");
+            pw.print(formatPid(consoleAppRoot, tf, ctx));
+            pw.println("</td>");
             pw.println("</tr>");
 
         }
@@ -371,7 +404,7 @@
     }
 
 
-    private void appendLogbackStatus(PrintWriter pw, LoggerStateContext ctx) {
+    private void appendLogbackStatus(final PrintWriter pw, final LoggerStateContext ctx) {
         pw.println("<div class='table'>");
 
         pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Logback Status</div>");
@@ -389,13 +422,21 @@
 
         pw.println("<tbody class='ui-widget-content'  >");
 
-        List<Status> statusList = ctx.loggerContext.getStatusManager().getCopyOfStatusList();
-        for (Status s : statusList) {
+        final List<Status> statusList = ctx.loggerContext.getStatusManager().getCopyOfStatusList();
+        for (final Status s : statusList) {
             pw.println("<tr>");
-            pw.println("<td class=\"date\">" + SDF.format(s.getDate()) + "</td>");
-            pw.println("<td class=\"level\">" + statusLevelAsString(s) + "</td>");
-            pw.println("<td>" + abbreviatedOrigin(s) + "</td>");
-            pw.println("<td>" + s.getMessage() + "</td>");
+            pw.print("<td class=\"date\">");
+            pw.print(SDF.format(s.getDate()));
+            pw.println("</td>");
+            pw.print("<td class=\"level\">");
+            pw.print(statusLevelAsString(s));
+            pw.println("</td>");
+            pw.print("<td>");
+            pw.print(XmlUtil.escapeXml(abbreviatedOrigin(s)));
+            pw.println("</td>");
+            pw.print("<td>");
+            pw.print(XmlUtil.escapeXml(s.getMessage()));
+            pw.println("</td>");
             pw.println("</tr>");
 
             // noinspection ThrowableResultOfMethodCallIgnored
@@ -409,9 +450,10 @@
         pw.println("</table>");
         pw.print("</div>");
         pw.println("</div>");
+        pw.println("<br />");
     }
 
-    private void appendLogbackMainConfig(PrintWriter pw) {
+    private void appendLogbackMainConfig(final PrintWriter pw) {
         pw.println("<div class='table'>");
         pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Logback Config</div>");
         pw.println("<table class='nicetable ui-widget'>");
@@ -435,11 +477,13 @@
             }
 
             pw.println("<tr>");
-            pw.println("<td>" + msg + "</td>");
+            pw.print("<td>");
+            pw.print(XmlUtil.escapeXml(msg));
+            pw.println("</td>");
             pw.println("</tr>");
 
             pw.println("<tr><td>");
-            String textContent = XmlUtil.escapeXml(XmlUtil.prettyPrint(source));
+            final String textContent = XmlUtil.escapeXml(XmlUtil.prettyPrint(source));
             pw.print("<pre class=\"prettyprint lang-xml\" style=\"border: 0px\">");
             pw.print(textContent);
             pw.print("</pre>");
@@ -458,7 +502,7 @@
         pw.println("</div>");
     }
 
-    private void appendLogbackFragments(PrintWriter pw, String consoleAppRoot) {
+    private void appendLogbackFragments(final PrintWriter pw, final String consoleAppRoot) {
         final Collection<ConfigSourceInfo> configSources = logbackManager.getConfigSourceTracker().getSources();
 
         if (configSources.isEmpty()) {
@@ -470,11 +514,13 @@
         pw.println("<table class='nicetable ui-widget'>");
         pw.println("<tbody class='ui-widget-content'>");
 
-        for (ConfigSourceInfo ci : configSources) {
+        for (final ConfigSourceInfo ci : configSources) {
             final String pid = ci.getReference().getProperty(Constants.SERVICE_ID).toString();
-            String url = createUrl(consoleAppRoot, "services", pid);
+            final String url = createUrl(consoleAppRoot, "services", pid);
             pw.println("<tr>");
-            pw.println("<td>" + url + "</td>");
+            pw.print("<td>");
+            pw.print(url);
+            pw.println("</td>");
             pw.println("</tr>");
 
             pw.println("<tr>");
@@ -494,7 +540,7 @@
     }
 
     /**
-     * Called internally by {@link AbstractWebConsolePlugin} to load resources.
+     * Called internally by AbstractWebConsolePlugin to load resources.
      * This particular implementation depends on the label. As example, if the
      * plugin is accessed as <code>/system/console/abc</code>, and the plugin
      * resources are accessed like <code>/system/console/abc/res/logo.gif</code>
@@ -692,14 +738,14 @@
         return (consoleAppRoot == null) ? "PID" : "Configuration";
     }
 
-    private static String createUrl(String consoleAppRoot, String subContext, String pid) {
+    private static String createUrl(final String consoleAppRoot, final String subContext, final String pid) {
         return createUrl(consoleAppRoot, subContext, pid, false);
     }
 
-    private static String createUrl(String consoleAppRoot, String subContext, String pid, boolean inlineEditable) {
+    private static String createUrl(final String consoleAppRoot, final String subContext, final String pid, final boolean inlineEditable) {
         // no recent web console, so just render the pid as the link
         if (consoleAppRoot == null) {
-            return "<a href=\"" + subContext + "/" + pid + "\">" + pid + "</a>";
+            return "<a href=\"" + subContext + "/" + XmlUtil.escapeXml(pid) + "\">" + XmlUtil.escapeXml(pid) + "</a>";
         }
 
         // recent web console has app root and hence we can use an image
@@ -708,7 +754,7 @@
             classAttr = "";
         }
 
-        return "<a " + classAttr + " href=\"" + subContext + "/" + pid + "\"><img src=\"" + consoleAppRoot
+        return "<a " + classAttr + " href=\"" + subContext + "/" + XmlUtil.escapeXml(pid) + "\"><img src=\"" + consoleAppRoot
                 + "/res/imgs/component_configure.png\" border=\"0\" /></a>";
     }
 
diff --git a/src/main/java/org/apache/sling/commons/log/logback/internal/util/XmlUtil.java b/src/main/java/org/apache/sling/commons/log/logback/internal/util/XmlUtil.java
index 0a36c58..daee418 100644
--- a/src/main/java/org/apache/sling/commons/log/logback/internal/util/XmlUtil.java
+++ b/src/main/java/org/apache/sling/commons/log/logback/internal/util/XmlUtil.java
@@ -57,7 +57,28 @@
         return "Source not found";
     }
 
-    public static String escapeXml(String xml) {
-        return xml.replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\\$", "&#37;");
+    public static String escapeXml(final String input) {
+        if (input == null) {
+            return null;
+        }
+
+        final StringBuilder b = new StringBuilder(input.length());
+        for(int i = 0;i  < input.length(); i++) {
+            final char c = input.charAt(i);
+            if (c == '&') {
+                b.append("&amp;");
+            } else if(c == '<') {
+                b.append("&lt;");
+            } else if(c == '>') {
+                b.append("&gt;");
+            } else if(c == '"') {
+                b.append("&quot;");
+            } else if(c == '\'') {
+                b.append("&apos;");
+            } else {
+                b.append(c);
+            }
+        }
+        return b.toString().replaceAll("\\$", "&#37;");
     }
 }