From ce043943d96cb3e2220e571b404c706a91baf3e0 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Sat, 23 Mar 2019 16:42:37 +0000
Subject: [PATCH] SusiDNS: Add import feature (ticket #2447) Box overlap issue
 remains todo, see ticket #2419

---
 apps/susidns/src/build.xml                    | 13 +++
 .../src/i2p/susi/dns/NamingServiceBean.java   | 92 ++++++++++++++++++-
 apps/susidns/src/jsp/addressbook.jsp          | 41 ++++++++-
 .../naming/SingleFileNamingService.java       |  4 +-
 .../resources/themes/susidns/dark/susidns.css | 14 +--
 .../themes/susidns/light/susidns.css          | 17 ++--
 6 files changed, 159 insertions(+), 22 deletions(-)

diff --git a/apps/susidns/src/build.xml b/apps/susidns/src/build.xml
index 4957c06389..d50d0fb8bb 100644
--- a/apps/susidns/src/build.xml
+++ b/apps/susidns/src/build.xml
@@ -97,6 +97,19 @@
         <replace file="WEB-INF/web-out.xml">
             <replacefilter token="&lt;!-- precompiled servlets --&gt;" value="${jspc.web.fragment}" />
         </replace>
+        <!-- Add multipart config to servlets that need them -->
+        <property name="__match1" value="&lt;servlet-class&gt;i2p.susi.dns.jsp." />
+        <property name="__match2" value="_jsp&lt;/servlet-class&gt;" />
+        <property name="__class1" value="${__match1}addressbook${__match2}" />
+        <property name="__multipart" value="&#10;
+       &lt;multipart-config&gt;&#10;
+           &lt;max-file-size&gt;67108864&lt;/max-file-size&gt;&#10;
+           &lt;max-request-size&gt;67108864&lt;/max-request-size&gt;&#10;
+           &lt;file-size-threshold&gt;262144&lt;/file-size-threshold&gt;&#10;
+       &lt;/multipart-config&gt;" />
+        <replace file="WEB-INF/web-out.xml">
+            <replacefilter token="${__class1}" value="${__class1}${__multipart}" />
+        </replace>
     </target>
 
     <uptodate property="precompilejsp.uptodate" targetfile="WEB-INF/web-out.xml">
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java
index 4426bed4e2..330b547957 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java
@@ -21,7 +21,11 @@
 
 package i2p.susi.dns;
 
+import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.Writer;
 import java.util.Arrays;
 import java.util.ArrayList;
@@ -34,9 +38,11 @@ import java.util.Properties;
 import java.util.SortedMap;
 
 import net.i2p.client.naming.NamingService;
+import net.i2p.client.naming.SingleFileNamingService;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
+import net.i2p.servlet.RequestWrapper;
 
 /**
  *  Talk to the NamingService API instead of modifying the hosts.txt files directly,
@@ -356,7 +362,7 @@ public class NamingServiceBean extends AddressbookBean
 		action = null;
 		
 		if( message.length() > 0 )
-			message = "<p class=\"messages\">" + message + "</p>";
+			message = styleMessage(message);
 		return message;
 	}
 
@@ -494,6 +500,90 @@ public class NamingServiceBean extends AddressbookBean
 		// No post-filtering for hosts.txt naming services. It is what it is.
 	}
 
+	/**
+	 *  @return messages about this action
+	 *  @since 0.9.40
+	 */
+	public String importFile(RequestWrapper wrequest) throws IOException {
+		String message = "";
+		InputStream in = wrequest.getInputStream("file");
+		OutputStream out = null;
+		File tmp = null;
+		SingleFileNamingService sfns = null;
+		try {
+			// non-null but zero bytes if no file entered, don't know why
+			if (in == null || in.available() <= 0) {
+				return styleMessage(_t("You must enter a file"));
+			}
+			// copy to temp file
+			tmp = new File(_context.getTempDir(), "susidns-import-" + _context.random().nextLong() + ".txt");
+			out = new FileOutputStream(tmp);
+			DataHelper.copy(in, out);
+                        in.close();
+                        in = null;
+                        out.close();
+                        out = null;
+			// new SingleFileNamingService
+			sfns = new SingleFileNamingService(_context, tmp.getAbsolutePath());
+			// getEntries, copy over
+			Map<String, Destination> entries = sfns.getEntries();
+			int count = entries.size();
+			if (count <= 0) {
+				return styleMessage(_t("No entries found in file"));
+			} else {
+				NamingService service = getNamingService();
+				int added = 0, dup = 0;
+				Properties nsOptions = new Properties();
+				nsOptions.setProperty("list", getFileName());
+	                        String now = Long.toString(_context.clock().now());
+	                       	nsOptions.setProperty("m", now);
+				String filename = wrequest.getFilename("file");
+				if (filename != null)
+					nsOptions.setProperty("s", _t("Imported from file {0}", filename));
+				else
+					nsOptions.setProperty("s", _t("Imported from file"));
+				for (Map.Entry<String, Destination> e : entries.entrySet()) {
+					String host = e.getKey();
+					Destination dest = e.getValue();
+					boolean ok = service.putIfAbsent(host, dest, nsOptions);
+					if (ok)
+						added++;
+					else
+						dup++;
+				}
+				StringBuilder buf = new StringBuilder(128);
+				if (added > 0)
+					buf.append(styleMessage(ngettext("Loaded {0} entry from file",
+				                                         "Loaded {0} entries from file",
+				                                         added)));
+				if (dup > 0)
+					buf.append(styleMessage(ngettext("Skipped {0} duplicate entry from file",
+				                                         "Skipped {0} duplicate entries from file",
+				                                         dup)));
+				return buf.toString();
+			}
+		} catch (IOException ioe) {
+			return styleMessage(_t("Import from file failed") + " - " + ioe);
+		} finally {
+			if (in != null)
+				try { in.close(); } catch (IOException ioe) {}
+			if (out != null)
+				try { out.close(); } catch (IOException ioe) {}
+			// shutdown SFNS
+			if (sfns != null)
+			    sfns.shutdown();
+			if (tmp != null)
+			    tmp.delete();
+		}
+	}
+
+	/**
+	 *  @since 0.9.40
+	 */
+	private static String styleMessage(String message) {
+		return "<p class=\"messages\">" + message + "</p>";
+	}
+	
 	/**
 	 *  @since 0.9.34
 	 */
diff --git a/apps/susidns/src/jsp/addressbook.jsp b/apps/susidns/src/jsp/addressbook.jsp
index 8d25e46a5d..02a8c1fb78 100644
--- a/apps/susidns/src/jsp/addressbook.jsp
+++ b/apps/susidns/src/jsp/addressbook.jsp
@@ -34,13 +34,18 @@
     response.setHeader("Referrer-Policy", "no-referrer");
     response.setHeader("Accept-Ranges", "none");
 
-%>
-<%@page pageEncoding="UTF-8"%>
-<%@ page contentType="text/html"%>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+%><%@page pageEncoding="UTF-8" contentType="text/html" import="net.i2p.servlet.RequestWrapper"
+%><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
 <jsp:useBean id="version" class="i2p.susi.dns.VersionBean" scope="application" />
 <jsp:useBean id="book" class="i2p.susi.dns.NamingServiceBean" scope="session" />
 <jsp:useBean id="intl" class="i2p.susi.dns.Messages" scope="application" />
+<%
+   String importMessages = null;
+   if (intl._t("Import").equals(request.getParameter("action"))) {
+       RequestWrapper wrequest = new RequestWrapper(request);
+       importMessages = book.importFile(wrequest);
+   }
+%>
 <jsp:setProperty name="book" property="*" />
 <jsp:setProperty name="book" property="resetDeletionMarks" value="1"/>
 <c:forEach items="${paramValues.checked}" var="checked">
@@ -75,7 +80,11 @@
 <h4><%=intl._t("Storage")%>: ${book.displayName}</h4>
 </div>
 
-<div id="messages">${book.messages}</div>
+<div id="messages">${book.messages}<%
+   if (importMessages != null) {
+       %><%=importMessages%><%
+   }
+%></div>
 
 ${book.loadBookMessages}
 
@@ -254,6 +263,28 @@ ${book.loadBookMessages}
 </div>
 </form>
 
+<% if (!book.getBook().equals("published")) { %>
+<form method="POST" action="addressbook" enctype="multipart/form-data" accept-charset="UTF-8">
+<input type="hidden" name="book" value="${book.book}">
+<input type="hidden" name="serial" value="<%=susiNonce%>">
+<input type="hidden" name="begin" value="0">
+<input type="hidden" name="end" value="49">
+<div id="import">
+<h3><%=intl._t("Import from hosts.txt file")%></h3>
+<table>
+<tr>
+<td><b><%=intl._t("File")%></b></td>
+<td><input name="file" type="file" accept=".txt" value="" /></td>
+</tr>
+</table>
+<p class="buttons">
+<input class="cancel" type="reset" value="<%=intl._t("Cancel")%>" >
+<input class="download" type="submit" name="action" value="<%=intl._t("Import")%>" >
+</p>
+</div>
+</form>
+<% } %>
+
 <div id="footer">
 <hr>
 <p class="footer">susidns v${version.version} &copy; <a href="${version.url}" target="_top">susi</a> 2005</p>
diff --git a/core/java/src/net/i2p/client/naming/SingleFileNamingService.java b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java
index 36e1360e88..48182c7bc1 100644
--- a/core/java/src/net/i2p/client/naming/SingleFileNamingService.java
+++ b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java
@@ -346,7 +346,7 @@ public class SingleFileNamingService extends NamingService {
     }
 
     /**
-     * @param options As follows:
+     * @param options null OK, or as follows:
      *                Key "search": return only those matching substring
      *                Key "startsWith": return only those starting with
      *                                  ("[0-9]" allowed)
@@ -413,7 +413,7 @@ public class SingleFileNamingService extends NamingService {
     /**
      *  Overridden since we store base64 natively.
      *
-     *  @param options As follows:
+     *  @param options null OK, or as follows:
      *                 Key "search": return only those matching substring
      *                 Key "startsWith": return only those starting with
      *                                   ("[0-9]" allowed)
diff --git a/installer/resources/themes/susidns/dark/susidns.css b/installer/resources/themes/susidns/dark/susidns.css
index 9e56db4a73..5d9e77fc3c 100644
--- a/installer/resources/themes/susidns/dark/susidns.css
+++ b/installer/resources/themes/susidns/dark/susidns.css
@@ -751,14 +751,14 @@ div#book, #emptybook {
      margin: 0;
 }
 
-div#add {
+div#add, div#import {
      border: 1px solid #2a5f29;
      padding: 0 0 10px;
      margin-top: 23px;
      background: #000;
 }
 
-#add h3 {
+#add h3, #import h3 {
      margin-top: -6px;
      margin-left: -1px;
      margin-right: -1px;
@@ -766,21 +766,23 @@ div#add {
      font-size: 10pt;
 }
 
-#add table {
+#add table, #import table {
      width: 100%;
      width: calc(100% - 1px);
      margin: -10px 10px 0 0;
 }
 
-#add td:first-child {
+#add td:first-child,
+#import table td:first-child {
      text-align: right;
 }
 
-#add td:last-child {
+#add td:last-child,
+#import td:last-child {
      width: 94%;
 }
 
-#add p.buttons {
+#add p.buttons, #import p.buttons {
      margin-top: 5px;
      border-top: 1px solid #2a5f29;
      padding-top: 5px;
diff --git a/installer/resources/themes/susidns/light/susidns.css b/installer/resources/themes/susidns/light/susidns.css
index 57c464b6b1..61ffb16a20 100644
--- a/installer/resources/themes/susidns/light/susidns.css
+++ b/installer/resources/themes/susidns/light/susidns.css
@@ -270,39 +270,40 @@ form[action="subscriptions"] #content {
      line-height: 130%;
 }
 
-div#add {
+div#add, div#import {
      border: 1px solid #7778bf;
      margin-top: -1px;
      padding: 0 15px;
      background: #fafaff;
 }
 
-.iframed #add {
+.iframed #add, .iframed #import {
      margin-top: 10px;
 }
 
-#add h3 {
+#add h3, #import h3 {
      border-bottom: 1px solid #7778bf;
      margin: 0 -15px;
      padding: 5px 10px;
 }
 
-#add table {
+#add table, #import table {
      width: 100%;
      margin: 5px 0;
 }
 
-#add table td:first-child {
+#add table td:first-child,
+#import table td:first-child {
      width: 50px;
      white-space: nowrap;
      text-align: right;
 }
 
-#add td {
+#add td, #import td {
      padding: 3px;
 }
 
-div#add p.buttons {
+div#add p.buttons, div#import p.buttons {
      border: 1px solid #7778bf;
      margin: 0 -16px -1px;
      background: linear-gradient(to bottom, #fff 50%, rgba(220,220,255,0.3)), repeating-linear-gradient(135deg, rgba(255,255,255,0.5) 2px, rgba(221, 221, 255, 0.3) 3px, #fff 5px), #fff !important;
@@ -327,7 +328,7 @@ div.help {
      font-size: 10pt;
 }
 
-div.help h3, #add h3 {
+div.help h3, #add h3, #import h3 {
      border: 1px solid #7778bf;
      padding: 5px 10px;
      margin: -1px -16px 0;
-- 
GitLab