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="<!-- precompiled servlets -->" value="${jspc.web.fragment}" /> </replace> + <!-- Add multipart config to servlets that need them --> + <property name="__match1" value="<servlet-class>i2p.susi.dns.jsp." /> + <property name="__match2" value="_jsp</servlet-class>" /> + <property name="__class1" value="${__match1}addressbook${__match2}" /> + <property name="__multipart" value=" + <multipart-config> + <max-file-size>67108864</max-file-size> + <max-request-size>67108864</max-request-size> + <file-size-threshold>262144</file-size-threshold> + </multipart-config>" /> + <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} © <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