From 2a900a8c5bb4099532e3f4dc79fcf9a1e8ea1f91 Mon Sep 17 00:00:00 2001 From: zzz <zzz@i2pmail.org> Date: Fri, 3 Dec 2021 06:26:14 -0500 Subject: [PATCH] i2psnark: Add torrent edit page Additional UI cleanup to follow --- .../java/src/org/klomp/snark/MetaInfo.java | 10 +- .../java/src/org/klomp/snark/Snark.java | 9 + .../org/klomp/snark/web/I2PSnarkServlet.java | 330 +++++++++++++++++- 3 files changed, 328 insertions(+), 21 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java index c191a8220f..e0f412297d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java @@ -82,8 +82,9 @@ public class MetaInfo * @param created_by may be null * @param url_list may be null * @param comment may be null + * @since public since 0.9.53, was package private */ - MetaInfo(String announce, String name, String name_utf8, List<List<String>> files, List<Long> lengths, + public MetaInfo(String announce, String name, String name_utf8, List<List<String>> files, List<Long> lengths, int piece_length, byte[] piece_hashes, long length, boolean privateTorrent, List<List<String>> announce_list, String created_by, List<String> url_list, String comment) { @@ -442,9 +443,12 @@ public class MetaInfo } /** - * Returns the piece hashes. Only used by storage so package local. + * Returns the piece hashes. + * + * @return not a copy, do not modify + * @since public since 0.9.53, was package private */ - byte[] getPieceHashes() + public byte[] getPieceHashes() { return piece_hashes; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index b95e9efabc..d669a674d7 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -1321,6 +1321,15 @@ public class Snark } } + /** + * Call after editing torrent. + * Caller must ensure infohash, files, etc. did not change. + * + * @since 0.9.53 + */ + public void replaceMetaInfo(MetaInfo metainfo) { + meta = metainfo; + } ///////////// Begin StorageListener methods diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index bf641e3940..d25205c1ce 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -3,6 +3,7 @@ package org.klomp.snark.web; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.PrintWriter; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; @@ -22,6 +23,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.TreeSet; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -35,8 +37,10 @@ import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.servlet.util.ServletUtil; +import net.i2p.util.FileUtil; import net.i2p.util.Log; import net.i2p.util.SecureFile; +import net.i2p.util.SecureFileOutputStream; import net.i2p.util.SystemVersion; import net.i2p.util.Translate; import net.i2p.util.UIMessages; @@ -251,11 +255,12 @@ public class I2PSnarkServlet extends BasicServlet { } } else { String base = addPaths(req.getRequestURI(), "/"); + boolean showEdit = req.getParameter("showEdit") != null; String listing = getListHTML(resource, base, true, method.equals("POST") ? req.getParameterMap() : null, - req.getParameter("sort")); + req.getParameter("sort"), showEdit); if (method.equals("POST")) { // P-R-G - sendRedirect(req, resp, ""); + sendRedirect(req, resp, showEdit ? "?showEdit" : ""); } else if (listing != null) { setHTMLHeaders(resp, cspNonce, true); resp.getWriter().write(listing); @@ -2984,8 +2989,8 @@ public class I2PSnarkServlet extends BasicServlet { * @return String of HTML or null if postParams != null * @since 0.7.14 */ - private String getListHTML(File xxxr, String base, boolean parent, Map<String, String[]> postParams, String sortParam) - throws IOException + private String getListHTML(File xxxr, String base, boolean parent, Map<String, String[]> postParams, + String sortParam, boolean showEdit) throws IOException { String decodedBase = decodePath(base); String title = decodedBase; @@ -3027,6 +3032,10 @@ public class I2PSnarkServlet extends BasicServlet { _manager.startTorrent(snark); } else if (postParams.get("recheck") != null) { _manager.recheckTorrent(snark); + } else if (postParams.get("editTorrent") != null) { + saveTorrentEdit(snark, postParams); + } else if (postParams.get("showEdit") != null) { + // P-R-G only } else { _manager.addMessage("Unknown command"); } @@ -3055,7 +3064,7 @@ public class I2PSnarkServlet extends BasicServlet { r = new File(""); } - boolean showStopStart = snark != null; + boolean showStopStart = snark != null && !showEdit; Storage storage = snark != null ? snark.getStorage() : null; boolean showPriority = storage != null && !storage.complete() && r.isDirectory(); @@ -3093,7 +3102,7 @@ public class I2PSnarkServlet extends BasicServlet { final boolean includeForm = showStopStart || showPriority || er || ec; if (includeForm) { buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n" + - "<input type=\"hidden\" name=\"nonce\" value=\"").append(_nonce).append("\" >\n"); + "<input type=\"hidden\" name=\"nonce\" value=\"").append(_nonce).append("\">\n"); if (sortParam != null) { buf.append("<input type=\"hidden\" name=\"sort\" value=\"") .append(DataHelper.stripHTML(sortParam)).append("\" >\n"); @@ -3137,7 +3146,7 @@ public class I2PSnarkServlet extends BasicServlet { String announce = null; MetaInfo meta = snark.getMetaInfo(); - if (meta != null) { + if (meta != null && !showEdit) { announce = meta.getAnnounce(); if (announce == null) announce = snark.getTrackerURL(); @@ -3218,7 +3227,7 @@ public class I2PSnarkServlet extends BasicServlet { toThemeImg(buf, "details"); buf.append("</td><td><b>") .append(_t("Comment")).append("</b></td><td>") - .append(DataHelper.stripHTML(com)) + .append(DataHelper.escapeHTML(com)) .append("</td></tr>\n"); } long dat = meta.getCreationDate(); @@ -3239,7 +3248,7 @@ public class I2PSnarkServlet extends BasicServlet { toThemeImg(buf, "details"); buf.append("</td><td><b>") .append(_t("Created By")).append("</b></td><td>") - .append(DataHelper.stripHTML(cby)) + .append(DataHelper.escapeHTML(cby)) .append("</td></tr>\n"); } long[] dates = _manager.getSavedAddedAndCompleted(snark); @@ -3390,7 +3399,7 @@ public class I2PSnarkServlet extends BasicServlet { buf.append("<b>").append(_t("Starting")).append("…</b>"); } else if (snark.isAllocating()) { buf.append("<b>").append(_t("Allocating")).append("…</b>"); - } else { + } else if (isTopLevel && !showEdit) { boolean isRunning = !snark.isStopped(); buf.append("<input type=\"submit\" value=\""); if (isRunning) @@ -3398,14 +3407,23 @@ public class I2PSnarkServlet extends BasicServlet { else buf.append(_t("Start")).append("\" name=\"start\" class=\"starttorrent\">\n"); buf.append("<input type=\"submit\" name=\"recheck\" value=\"").append(_t("Force Recheck")); - if (isRunning) + if (isRunning) { buf.append("\" class=\"disabled\" disabled=\"disabled\" title=\"") - .append(_t("Stop the torrent in order to check file integrity")) - .append("\">\n"); - else + .append(_t("Torrent must be stopped")); + } else { + buf.append("\" class=\"reload\" title=\"") + .append(_t("Check integrity of the downloaded files")); + } + buf.append("\">\n" + + "<input type=\"submit\" name=\"showEdit\" value=\"").append(_t("Edit Torrent")); + if (isRunning) { + buf.append("\" class=\"disabled\" disabled=\"disabled\" title=\"") + .append(_t("Torrent must be stopped")); + } else { buf.append("\" class=\"reload\" title=\"") - .append(_t("Check integrity of the downloaded files")) - .append("\">\n"); + .append(_t("Add or remove trackers")); + } + buf.append("\">\n"); } boolean showInOrder = storage != null && !storage.complete() && meta != null; @@ -3439,6 +3457,13 @@ public class I2PSnarkServlet extends BasicServlet { } buf.append("</table>\n"); + if (snark != null && isTopLevel && showEdit) { + // Edit torrent. Show edit section only. + displayTorrentEdit(snark, base, buf); + buf.append("</form></div></div></center></body></html>"); + return buf.toString(); + } + if (snark != null && !r.exists()) { // fixup TODO buf.append("<table class=\"resourceError\" id=\"DoesNotExist\"><tr><th colspan=\"2\">") @@ -3481,7 +3506,7 @@ public class I2PSnarkServlet extends BasicServlet { displayComments(snark, er, ec, esc, buf); if (includeForm) buf.append("</form>"); - buf.append("</div></div></body></html>"); + buf.append("</div></div></center></body></html>"); return buf.toString(); } @@ -3778,7 +3803,7 @@ public class I2PSnarkServlet extends BasicServlet { // for stop/start/check if (includeForm) buf.append("</form>"); - buf.append("</div></div></body></html>"); + buf.append("</div></div></center></body></html>"); return buf.toString(); } @@ -4150,6 +4175,134 @@ public class I2PSnarkServlet extends BasicServlet { buf.append("</div>"); } + /** + * @param snark non-null + * @since 0.9.53 + */ + private void displayTorrentEdit(Snark snark, String base, StringBuilder buf) { + MetaInfo meta = snark.getMetaInfo(); + if (meta == null) + return; + buf.append("<div id=\"snarkCommentSection\"><table class=\"snarkTorrentInfo\">\n<tr><th colspan=\"5\">") + .append(_t("Edit Torrent")) + .append("</th></tr>"); + boolean isRunning = !snark.isStopped(); + if (isRunning) { + // shouldn't happen + buf.append("<tr><td colspan=\"5\">") + .append(_t("Torrent must be stopped")) + .append("</td></tr></table></div></form>"); + return; + } + String announce = meta.getAnnounce(); + if (announce == null) + announce = snark.getTrackerURL(); + if (announce != null) { + // strip non-i2p trackers + if (!isI2PTracker(announce)) + announce = null; + } + List<List<String>> alist = meta.getAnnounceList(); + Set<String> annlist = new TreeSet<String>(); + if (alist != null && !alist.isEmpty()) { + // strip non-i2p trackers + for (List<String> alist2 : alist) { + for (String s : alist2) { + if (isI2PTracker(s)) + annlist.add(s); + } + } + } + if (announce != null) + annlist.add(announce); + if (!annlist.isEmpty()) { + buf.append("<tr><td colspan=\"3\"></td><td>").append("Primary").append("</td><td>") + .append("Delete").append("</td></tr>"); + for (String s : annlist) { + int hc = s.hashCode(); + buf.append("<tr><td>"); + toThemeImg(buf, "details"); + buf.append("</td><td><b>") + .append(_t("Tracker")).append("</b></td><td>"); + s = DataHelper.stripHTML(s); + buf.append("<span class=\"info_tracker\">"); + buf.append(getShortTrackerLink(s, snark.getInfoHash())); + buf.append("</span> "); + //buf.append(s); + buf.append("</td><td>"); + buf.append("<input type=\"radio\" class=\"optbox\" name=\"primary\" "); + if (s.equals(announce)) + buf.append("checked=\"checked\" "); + buf.append("value=\"").append(hc); + buf.append("\"></td><td>"); + buf.append("<input type=\"checkbox\" class=\"optbox\" name=\"trdelete.") + .append(hc).append("\" title=\"").append(_t("Mark for deletion")).append("\">"); + buf.append("</td></tr>\n"); + } + } + + List<Tracker> newTrackers = _manager.getSortedTrackers(); + for (Iterator<Tracker> iter = newTrackers.iterator(); iter.hasNext(); ) { + Tracker t = iter.next(); + String announceURL = t.announceURL.replace("=", "="); + if (announceURL.equals(announce) || annlist.contains(announceURL)) + iter.remove(); + } + if (!newTrackers.isEmpty()) { + buf.append("<tr><td colspan=\"3\"></td><td>").append("Primary").append("</td><td>") + .append("Add").append("</td></tr>"); + for (Tracker t : newTrackers) { + String name = t.name; + int hc = t.announceURL.hashCode(); + String announceURL = t.announceURL.replace("=", "="); + buf.append("<tr><td>"); + toThemeImg(buf, "details"); + buf.append("</td><td><b>") + .append(_t("Add Tracker")).append("</b></td><td>"); + buf.append(name); + buf.append("</td><td><input type=\"radio\" class=\"optbox\" name=\"primary\" value=\""); + buf.append(hc); + buf.append("\"></td><td>"); + buf.append("<input type=\"checkbox\" class=\"optbox\" id=\"").append(name).append("\" name=\"tradd.") + .append(hc).append("\" title=\"").append(_t("Add tracker")).append("\"> ") + .append("</td><td></td></tr>\n"); + } + } + + String com = meta.getComment(); + if (com == null) { + com = ""; + } else if (com.length() > 0) { + com = DataHelper.escapeHTML(com); + } + buf.append("<tr><td>"); + toThemeImg(buf, "details"); + buf.append("</td><td><b>") + .append(_t("Comment")).append("</b></td>"); + buf.append("<td colspan=\"2\" id=\"addCommentText\"><textarea name=\"nofilter_newTorrentComment\" cols=\"88\" rows=\"4\">") + .append(com).append("</textarea></td><td></td>"); + buf.append("</tr>\n"); + + String cb = meta.getCreatedBy(); + if (cb == null) { + cb = ""; + } else if (cb.length() > 0) { + cb = DataHelper.escapeHTML(cb); + } + buf.append("<tr><td>"); + toThemeImg(buf, "details"); + buf.append("</td><td><b>") + .append(_t("Created By")).append("</b></td>"); + buf.append("<td id=\"editTorrentCreatedBy\"><input type=\"text\" name=\"nofilter_newTorrentCreatedBy\" cols=\"44\" rows=\"1\" value=\"") + .append(cb).append("\"></td></tr>"); + + buf.append("<tr id=\"torrentInfoControl\"><td colspan=\"5\">"); + buf.append("<input type=\"submit\" name=\"editTorrent\" value=\""); + buf.append(_t("Save Changes")); + buf.append("\" class=\"accept\"></td></tr>\n"); + buf.append("</table></div>"); + } + /** * @param so null ok * @return query string or "" @@ -4370,6 +4523,147 @@ public class I2PSnarkServlet extends BasicServlet { _manager.setSavedCommentsEnabled(snark, yes); } + /** + * @since 0.9.53 + */ + private void saveTorrentEdit(Snark snark, Map<String, String[]> postParams) { + if (!snark.isStopped()) { + // shouldn't happen + _manager.addMessage(_t("Torrent must be stopped")); + return; + } + List<Integer> toAdd = new ArrayList<Integer>(); + List<Integer> toDel = new ArrayList<Integer>(); + Integer primary = null; + String newComment = ""; + String newCreatedBy = ""; + for (Map.Entry<String, String[]> entry : postParams.entrySet()) { + String key = entry.getKey(); + String val = entry.getValue()[0]; // jetty arrays + if (key.startsWith("tradd.")) { + try { + toAdd.add(Integer.parseInt(key.substring(6))); + } catch (NumberFormatException nfe) {} + } else if (key.startsWith("trdelete.")) { + try { + toDel.add(Integer.parseInt(key.substring(9))); + } catch (NumberFormatException nfe) {} + } else if (key.equals("primary")) { + try { + primary = Integer.parseInt(val); + } catch (NumberFormatException nfe) {} + } else if (key.equals("nofilter_newTorrentComment")) { + newComment = val.trim(); + } else if (key.equals("nofilter_newTorrentCreatedBy")) { + newCreatedBy = val.trim(); + } + } + MetaInfo meta = snark.getMetaInfo(); + if (meta == null) { + // shouldn't happen + _manager.addMessage("Can't edit magnet"); + return; + } + String oldPrimary = meta.getAnnounce(); + String oldComment = meta.getComment(); + if (oldComment == null) + oldComment = ""; + String oldCreatedBy = meta.getCreatedBy(); + if (oldCreatedBy == null) + oldCreatedBy = ""; + if (toAdd.isEmpty() && toDel.isEmpty() && + (primary == null || primary.equals(oldPrimary)) && + oldComment.equals(newComment) && + oldCreatedBy.equals(newCreatedBy)) { + _manager.addMessage("No changes to torrent, not saved"); + return; + } + List<List<String>> alist = meta.getAnnounceList(); + Set<String> annlist = new TreeSet<String>(); + if (alist != null && !alist.isEmpty()) { + // strip non-i2p trackers + for (List<String> alist2 : alist) { + for (String s : alist2) { + if (isI2PTracker(s)) + annlist.add(s); + } + } + } + if (oldPrimary != null) + annlist.add(oldPrimary); + List<Tracker> newTrackers = _manager.getSortedTrackers(); + for (Integer i : toDel) { + int hc = i.intValue(); + for (Iterator<String> iter = annlist.iterator(); iter.hasNext(); ) { + String s = iter.next(); + if (s.hashCode() == hc) + iter.remove(); + } + } + for (Integer i : toAdd) { + int hc = i.intValue(); + for (Tracker t : newTrackers) { + if (t.announceURL.hashCode() == hc) { + annlist.add(t.announceURL); + break; + } + } + } + String thePrimary = oldPrimary; + if (primary != null) { + int hc = primary.intValue(); + for (String s : annlist) { + if (s.hashCode() == hc) { + thePrimary = s; + break; + } + } + } + List<List<String>> newAnnList; + if (annlist.isEmpty()) { + newAnnList = null; + thePrimary = null; + } else { + List<String> aalist = new ArrayList<String>(annlist); + newAnnList = Collections.singletonList(aalist); + if (!aalist.contains(thePrimary)) + thePrimary = aalist.get(0); + } + if (newComment.equals("")) + newComment = null; + if (newCreatedBy.equals("")) + newCreatedBy = null; + MetaInfo newMeta = new MetaInfo(thePrimary, meta.getName(), null, meta.getFiles(), meta.getLengths(), + meta.getPieceLength(0), meta.getPieceHashes(), meta.getTotalLength(), meta.isPrivate(), + newAnnList, newCreatedBy, meta.getWebSeedURLs(), newComment); + if (!DataHelper.eq(meta.getInfoHash(), newMeta.getInfoHash())) { + // shouldn't happen + _manager.addMessage("Torrent edit failed, infohash mismatch"); + return; + } + File f = new File(_manager.util().getTempDir(), "edit-" + _manager.util().getContext().random().nextLong() + ".torrent"); + OutputStream out = null; + try { + out = new SecureFileOutputStream(f); + out.write(newMeta.getTorrentData()); + out.close(); + boolean ok = FileUtil.rename(f, new File(snark.getName())); + if (!ok) { + _manager.addMessage("Save edit changes failed"); + return; + } + } catch (IOException ioe) { + try { if (out != null) out.close(); } catch (IOException ioe2) {} + _manager.addMessage("Save edit changes failed: " + ioe); + return; + } finally { + f.delete(); + } + snark.replaceMetaInfo(newMeta); + _manager.addMessage("Torrent changes saved"); + } + + /** @since 0.9.32 */ private static boolean noCollapsePanels(HttpServletRequest req) { // check for user agents that can't toggle the collapsible panels... -- GitLab