From acdaa60de32e925dcc84cf36326c60d3a7b8f1a8 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Wed, 3 Feb 2016 13:20:22 +0000
Subject: [PATCH] Console: Custom icons for non-webapp plugins, from cacapo
 (ticket #1550)

---
 .../router/web/CodedIconRendererServlet.java  | 90 +++++++++++++++++++
 .../src/net/i2p/router/web/NavHelper.java     | 15 ++++
 .../src/net/i2p/router/web/PluginStarter.java | 15 ++++
 apps/routerconsole/jsp/web.xml                | 12 +++
 4 files changed, 132 insertions(+)
 create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/CodedIconRendererServlet.java

diff --git a/apps/routerconsole/java/src/net/i2p/router/web/CodedIconRendererServlet.java b/apps/routerconsole/java/src/net/i2p/router/web/CodedIconRendererServlet.java
new file mode 100644
index 0000000000..4745d945b8
--- /dev/null
+++ b/apps/routerconsole/java/src/net/i2p/router/web/CodedIconRendererServlet.java
@@ -0,0 +1,90 @@
+package net.i2p.router.web;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.File;
+import java.util.Arrays;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.GenericServlet;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import net.i2p.data.Base64;
+ 
+
+ /**
+  *
+  * @author cacapo
+  */
+
+public class CodedIconRendererServlet extends HttpServlet {
+ 
+    public static final long serialVersionUID = 16851750L;
+    
+    String base = net.i2p.I2PAppContext.getGlobalContext().getBaseDir().getAbsolutePath();
+    String file = "docs" + java.io.File.separatorChar + "themes" + java.io.File.separatorChar + "console" +  java.io.File.separatorChar + "images" + java.io.File.separatorChar + "plugin.png";
+       
+
+
+     @Override
+
+     protected void service(HttpServletRequest srq, HttpServletResponse srs) throws ServletException, IOException {
+         byte[] data;
+         String name = srq.getParameter("plugin");
+         data  = NavHelper.getBinary(name);
+         
+         //set as many headers as are common to any outcome
+         
+         srs.setContentType("image/png");
+         srs.setDateHeader("Expires", net.i2p.I2PAppContext.getGlobalContext().clock().now() + 86400000l);
+         srs.setHeader("Cache-Control", "public, max-age=86400");
+         OutputStream os = srs.getOutputStream();
+         
+         //Binary data is present
+         if(data != null){
+             srs.setHeader("Content-Length", Integer.toString(data.length));
+             int content = Arrays.hashCode(data);
+             int chksum = srq.getIntHeader("If-None-Match");//returns -1 if no such header
+             //Don't render if icon already present
+             if(content != chksum){
+                 srs.setIntHeader("ETag", content);
+                 try{
+                     os.write(data);
+                     os.flush();
+                     os.close();
+                 }catch(IOException e){
+                     net.i2p.I2PAppContext.getGlobalContext().logManager().getLog(getClass()).warn("Error writing binary image data for plugin", e);
+                 }
+             } else {
+                 srs.sendError(304, "Not Modified");
+             }
+         } else {
+             //Binary data is not present but must be substituted by file on disk
+             File pfile = new java.io.File(base, file);
+             srs.setHeader("Content-Length", Long.toString(pfile.length()));
+             try{
+                 long lastmod = pfile.lastModified();
+                 if(lastmod > 0){
+                     long iflast = srq.getDateHeader("If-Modified-Since");
+                     if(iflast >= ((lastmod/1000) * 1000)){
+                         srs.sendError(304, "Not Modified");
+                     } else {
+                         srs.setDateHeader("Last-Modified", lastmod);
+                         net.i2p.util.FileUtil.readFile(file, base, os); 
+                     }
+                     
+                 }
+             } catch(java.io.IOException e) {
+                 if (!srs.isCommitted()) {
+                     srs.sendError(403, e.toString());
+                 } else {
+                     net.i2p.I2PAppContext.getGlobalContext().logManager().getLog(getClass()).warn("Error serving plugin.png", e);
+                     throw e;
+                 }
+             }
+             
+         }
+     }
+}
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java
index 713520c94a..02e397df1a 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java
@@ -13,6 +13,7 @@ public class NavHelper {
     private static final Map<String, String> _apps = new ConcurrentHashMap<String, String>(4);
     private static final Map<String, String> _tooltips = new ConcurrentHashMap<String, String>(4);
     private static final Map<String, String> _icons = new ConcurrentHashMap<String, String>(4);
+    private static final Map<String, byte[]> _binary = new ConcurrentHashMap<String, byte[]>(4);
     
     /**
      * To register a new client application so that it shows up on the router
@@ -40,6 +41,20 @@ public class NavHelper {
         _icons.remove(name);
     }
     
+
+    public static byte[] getBinary(String name){
+        if(name != null)
+            return _binary.get(name);
+        else
+            return null;
+    }
+
+
+    public static void setBinary(String name, byte[] arr){
+        _binary.put(name, arr);
+    }
+
+
     /**
      *  Translated string is loaded by PluginStarter
      *  @param ctx unused
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
index 2af0d5df46..62cced0694 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java
@@ -22,6 +22,7 @@ import net.i2p.I2PAppContext;
 import net.i2p.app.ClientApp;
 import net.i2p.app.ClientAppState;
 import net.i2p.data.DataHelper;
+import net.i2p.data.Base64;
 import net.i2p.router.RouterContext;
 import net.i2p.router.RouterVersion;
 import net.i2p.router.startup.ClientAppConfig;
@@ -353,6 +354,20 @@ public class PluginStarter implements Runnable {
             }
         }
 
+
+    //handle console icons for plugins without web-resources through prop icon-code
+    String fullprop = props.getProperty("icon-code");
+    if(fullprop != null && fullprop.length() > 1){
+        byte[] decoded = Base64.decode(fullprop);
+        if(decoded != null) {
+            NavHelper.setBinary(appName, decoded);
+            iconfile = "/Plugins/pluginicon?plugin=" + appName;
+        } else {
+            iconfile = "/themes/console/images/plugin.png";
+        }
+    }
+
+
         // load and start things in clients.config
         File clientConfig = new File(pluginDir, "clients.config");
         if (clientConfig.exists()) {
diff --git a/apps/routerconsole/jsp/web.xml b/apps/routerconsole/jsp/web.xml
index ea183c8356..44e40c86e5 100644
--- a/apps/routerconsole/jsp/web.xml
+++ b/apps/routerconsole/jsp/web.xml
@@ -14,6 +14,18 @@
     </filter-mapping>
 
     <!-- precompiled servlets -->
+
+    <servlet>
+      <servlet-name>net.i2p.router.web.CodedIconRendererServlet</servlet-name>
+      <servlet-class>net.i2p.router.web.CodedIconRendererServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+      <servlet-name>net.i2p.router.web.CodedIconRendererServlet</servlet-name>
+      <url-pattern>/Plugins/*</url-pattern>
+    </servlet-mapping>
+
+
     
     <!-- yeah, i'm lazy, using a jsp instead of a servlet.. -->
     <servlet-mapping> 
-- 
GitLab