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 34f9935ac..0d5ae0996 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -194,22 +194,8 @@ public class I2PSnarkServlet extends BasicServlet { resp.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"); resp.setHeader("X-XSS-Protection", "1; mode=block"); - String peerParam = req.getParameter("p"); - String stParam = req.getParameter("st"); - String peerString; - if (peerParam == null || (!_manager.util().connected()) || - peerParam.replaceAll("[a-zA-Z0-9~=-]", "").length() > 0) { // XSS - peerString = ""; - } else { - peerString = "?p=" + DataHelper.stripHTML(peerParam); - } - if (stParam != null && !stParam.equals("0")) { - stParam = DataHelper.stripHTML(stParam); - if (peerString.length() > 0) - peerString += "&st=" + stParam; - else - peerString = "?st="+ stParam; - } + String pOverride = _manager.util().connected() ? null : ""; + String peerString = getQueryString(req, pOverride, null, null); // AJAX for mainsection if ("/.ajax/xhr1.html".equals(path)) { @@ -292,6 +278,7 @@ public class I2PSnarkServlet extends BasicServlet { out.write(_("Configuration")); else out.write(_("Anonymous BitTorrent Client")); + String peerParam = req.getParameter("p"); if ("2".equals(peerParam)) out.write(" | Debug Mode"); out.write("\n"); @@ -413,13 +400,7 @@ public class I2PSnarkServlet extends BasicServlet { boolean isForm = _manager.util().connected() || !snarks.isEmpty(); if (isForm) { out.write("
\n"); - out.write("\n"); - // don't lose peer setting - if (peerParam != null) - out.write("\n"); - // ...or st setting - if (stParam != null) - out.write("\n"); + writeHiddenInputs(out, req, null); } out.write(TABLE_HEADER); @@ -441,18 +422,19 @@ public class I2PSnarkServlet extends BasicServlet { } int pageSize = Math.max(_manager.getPageSize(), 5); - out.write(""); + String sort = ("2".equals(currentSort)) ? "-2" : "2"; + out.write("\"");\n"); + out.write("\">\n"); if (_manager.util().connected() && !snarks.isEmpty()) { out.write(" "); out.write(""); } else { - out.write("?p=1"); - if (stParam != null) { - out.write("&st="); - out.write(stParam); - } + // enable peer view + out.write(getQueryString(req, "1", null, null)); out.write("\">"); out.write("
\n"); } out.write("\n"); - out.write("\"");\n"); + out.write("\">
\n"); if (total > 0 && (start > 0 || total > pageSize)) { - writePageNav(out, start, pageSize, total, peerParam, noThinsp); + writePageNav(out, req, start, pageSize, total, noThinsp); } out.write("\n"); if (_manager.util().connected() && !snarks.isEmpty()) { - out.write("\"");"); + out.write("\">"); } out.write("\n"); - out.write("\"");"); + out.write("\">"); out.write("\n"); if (!snarks.isEmpty()) { - out.write("\"");"); + out.write("\">"); } out.write("\n"); if (_manager.util().connected() && !snarks.isEmpty()) { - out.write("\"");"); + out.write("\">"); } out.write("\n"); if (_manager.util().connected() && !snarks.isEmpty()) { - out.write("\"");"); + out.write("\">"); } out.write("\n"); @@ -580,12 +572,11 @@ public class I2PSnarkServlet extends BasicServlet { String uri = _contextPath + '/'; boolean showDebug = "2".equals(peerParam); - String stParamStr = stParam == null ? "" : "&st=" + stParam; for (int i = 0; i < total; i++) { Snark snark = snarks.get(i); boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.getInfoHash()).equals(peerParam); boolean hide = i < start || i >= start + pageSize; - displaySnark(out, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug, hide, stParamStr); + displaySnark(out, req, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug, hide); } if (total == 0) { @@ -636,17 +627,105 @@ public class I2PSnarkServlet extends BasicServlet { return start == 0; } + /** + * hidden inputs for nonce and paramters p, st, and sort + * + * @param out writes to it + * @param action if non-null, add it as the action + * @since 0.9.16 + */ + private void writeHiddenInputs(PrintWriter out, HttpServletRequest req, String action) { + StringBuilder buf = new StringBuilder(256); + writeHiddenInputs(buf, req, action); + out.write(buf.toString()); + } + + /** + * hidden inputs for nonce and paramters p, st, and sort + * + * @param out appends to it + * @param action if non-null, add it as the action + * @since 0.9.16 + */ + private void writeHiddenInputs(StringBuilder buf, HttpServletRequest req, String action) { + buf.append("\n"); + String peerParam = req.getParameter("p"); + if (peerParam != null) { + buf.append("\n"); + } + String stParam = req.getParameter("st"); + if (stParam != null) { + buf.append("\n"); + } + String soParam = req.getParameter("sort"); + if (soParam != null) { + buf.append("\n"); + } + if (action != null) { + buf.append("\n"); + } + } + + /** + * Build HTML-escaped and stripped query string + * + * @param p override or "" for default or null to keep the same as in req + * @param st override or "" for default or null to keep the same as in req + * @param so override or "" for default or null to keep the same as in req + * @return non-null, possibly empty + * @since 0.9.16 + */ + private static String getQueryString(HttpServletRequest req, String p, String st, String so) { + StringBuilder buf = new StringBuilder(64); + if (p == null) { + p = req.getParameter("p"); + if (p != null) + p = DataHelper.stripHTML(p); + } + if (p != null && !p.equals("")) + buf.append("?p=").append(p); + if (so == null) { + so = req.getParameter("sort"); + if (so != null) + so = DataHelper.stripHTML(so); + } + if (so != null && !so.equals("")) { + if (buf.length() <= 0) + buf.append("?sort="); + else + buf.append("&sort="); + buf.append(so); + } + if (st == null) { + st = req.getParameter("st"); + if (st != null) + st = DataHelper.stripHTML(st); + } + if (st != null && !st.equals("")) { + if (buf.length() <= 0) + buf.append("?st="); + else + buf.append("&st="); + buf.append(st); + } + return buf.toString(); + } + /** * @since 0.9.6 */ - private void writePageNav(PrintWriter out, int start, int pageSize, int total, - String peerParam, boolean noThinsp) { + private void writePageNav(PrintWriter out, HttpServletRequest req, int start, int pageSize, int total, + boolean noThinsp) { // Page nav if (start > 0) { // First out.write("" + "\""" + @@ -655,9 +734,9 @@ public class I2PSnarkServlet extends BasicServlet { //if (prev > 0) { if (true) { // Back - out.write("  0) ? Integer.toString(prev) : ""; + out.write(getQueryString(req, null, sprev, null)); out.write("\">" + "\""" + @@ -690,9 +769,8 @@ public class I2PSnarkServlet extends BasicServlet { //if (next + pageSize < total) { if (true) { // Next - out.write(" " + "\""" + @@ -700,9 +778,8 @@ public class I2PSnarkServlet extends BasicServlet { } // Last int last = ((total - 1) / pageSize) * pageSize; - out.write(" " + "\""" + @@ -1190,34 +1267,16 @@ public class I2PSnarkServlet extends BasicServlet { return buf.toString(); } - /** - * Sort alphabetically in current locale, ignore case, ignore leading "the " - * (I guess this is worth it, a lot of torrents start with "The " - * @since 0.7.14 - */ - private static class TorrentNameComparator implements Comparator, Serializable { - - public int compare(Snark l, Snark r) { - // put downloads and magnets first - if (l.getStorage() == null && r.getStorage() != null) - return -1; - if (l.getStorage() != null && r.getStorage() == null) - return 1; - String ls = l.getBaseName(); - String llc = ls.toLowerCase(Locale.US); - if (llc.startsWith("the ") || llc.startsWith("the.") || llc.startsWith("the_")) - ls = ls.substring(4); - String rs = r.getBaseName(); - String rlc = rs.toLowerCase(Locale.US); - if (rlc.startsWith("the ") || rlc.startsWith("the.") || rlc.startsWith("the_")) - rs = rs.substring(4); - return Collator.getInstance().compare(ls, rs); - } - } - private List getSortedSnarks(HttpServletRequest req) { ArrayList rv = new ArrayList(_manager.getTorrents()); - Collections.sort(rv, new TorrentNameComparator()); + int sort = 0; + String ssort = req.getParameter("sort"); + if (ssort != null) { + try { + sort = Integer.parseInt(ssort); + } catch (NumberFormatException nfe) {} + } + Collections.sort(rv, Sorters.getComparator(sort)); return rv; } @@ -1229,11 +1288,11 @@ public class I2PSnarkServlet extends BasicServlet { * * @param stats in/out param (totals) * @param statsOnly if true, output nothing, update stats only - * @param stParam non null; empty or e.g. &st=10 */ - private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers, - boolean isDegraded, boolean noThinsp, boolean showDebug, boolean statsOnly, - String stParam) throws IOException { + private void displaySnark(PrintWriter out, HttpServletRequest req, + Snark snark, String uri, int row, long stats[], boolean showPeers, + boolean isDegraded, boolean noThinsp, boolean showDebug, boolean statsOnly) + throws IOException { // stats long uploaded = snark.getUploaded(); stats[0] += snark.getDownloaded(); @@ -1335,7 +1394,7 @@ public class I2PSnarkServlet extends BasicServlet { if (curPeers > 0 && !showPeers) statusString = "\"\"" + "" + txt + - ": " + + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + ""; else @@ -1351,7 +1410,7 @@ public class I2PSnarkServlet extends BasicServlet { if (isRunning && curPeers > 0 && downBps > 0 && !showPeers) statusString = "\"\"" + "" + _("OK") + - ": " + + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + ""; else if (isRunning && curPeers > 0 && downBps > 0) @@ -1362,7 +1421,7 @@ public class I2PSnarkServlet extends BasicServlet { else if (isRunning && curPeers > 0 && !showPeers) statusString = "\"\"" + "" + _("Stalled") + - ": " + + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + ""; else if (isRunning && curPeers > 0) @@ -1484,7 +1543,8 @@ public class I2PSnarkServlet extends BasicServlet { } else if (isRunning) { // Stop Button if (isDegraded) - out.write("\n"); // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file out.write("\n"); - out.write("\n"); - out.write("\n"); - // don't lose peer setting - String peerParam = req.getParameter("p"); - if (peerParam != null) - out.write("\n"); + writeHiddenInputs(out, req, "Add"); out.write("
"); out.write("\"\" "); out.write(_("Add Torrent")); @@ -1858,12 +1916,7 @@ public class I2PSnarkServlet extends BasicServlet { out.write("
\n"); // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file out.write("\n"); - out.write("\n"); - out.write("\n"); - // don't lose peer setting - String peerParam = req.getParameter("p"); - if (peerParam != null) - out.write("\n"); + writeHiddenInputs(out, req, "Create"); out.write(""); out.write("\"\" "); out.write(_("Create Torrent")); @@ -1929,10 +1982,9 @@ public class I2PSnarkServlet extends BasicServlet { //int seedPct = 0; out.write("\n" + - "
\n" + - "\n" + - "\n" + - "" + + "
\n"); + writeHiddenInputs(out, req, "Save"); + out.write("" + "\"\" "); out.write(_("Configuration")); out.write("
\n" + @@ -2115,10 +2167,9 @@ public class I2PSnarkServlet extends BasicServlet { private void writeTrackerForm(PrintWriter out, HttpServletRequest req) throws IOException { StringBuilder buf = new StringBuilder(1024); buf.append("\n" + - "
\n" + - "\n" + - "\n" + - "" + + "
\n"); + writeHiddenInputs(buf, req, "Save2"); + buf.append("" + "\"\" "); buf.append(_("Trackers")); buf.append("
\n" + diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java b/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java new file mode 100644 index 000000000..e95cc2961 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java @@ -0,0 +1,261 @@ +package org.klomp.snark.web; + +import java.io.Serializable; +import java.text.Collator; +import java.util.Collections; +import java.util.Comparator; +import java.util.Locale; + +import org.klomp.snark.Snark; + +/** + * Comparators for various columns + * + * @since 0.9.16 moved from I2PSnarkservlet + */ +class Sorters { + + public static Comparator getComparator(int type) { + boolean rev = type < 0; + Comparator rv; + switch (type) { + + case -1: + case 0: + case 1: + default: + rv = new TorrentNameComparator(); + if (rev) + rv = Collections.reverseOrder(rv); + break; + + case -2: + case 2: + rv = new StatusComparator(rev); + break; + + case -3: + case 3: + rv = new PeersComparator(rev); + break; + + case -4: + case 4: + rv = new ETAComparator(rev); + break; + + case -5: + case 5: + rv = new SizeComparator(rev); + break; + + case -6: + case 6: + rv = new DownloadedComparator(rev); + break; + + case -7: + case 7: + rv = new UploadedComparator(rev); + break; + + case -8: + case 8: + rv = new DownRateComparator(rev); + break; + + case -9: + case 9: + rv = new UpRateComparator(rev); + break; + + case -10: + case 10: + rv = new RemainingComparator(rev); + break; + + } + return rv; + } + + + /** + * Sort alphabetically in current locale, ignore case, ignore leading "the " + * (I guess this is worth it, a lot of torrents start with "The " + * @since 0.7.14 + */ + private static class TorrentNameComparator implements Comparator, Serializable { + + public int compare(Snark l, Snark r) { + return comp(l, r); + } + + public static int comp(Snark l, Snark r) { + // put downloads and magnets first + if (l.getStorage() == null && r.getStorage() != null) + return -1; + if (l.getStorage() != null && r.getStorage() == null) + return 1; + String ls = l.getBaseName(); + String llc = ls.toLowerCase(Locale.US); + if (llc.startsWith("the ") || llc.startsWith("the.") || llc.startsWith("the_")) + ls = ls.substring(4); + String rs = r.getBaseName(); + String rlc = rs.toLowerCase(Locale.US); + if (rlc.startsWith("the ") || rlc.startsWith("the.") || rlc.startsWith("the_")) + rs = rs.substring(4); + return Collator.getInstance().compare(ls, rs); + } + } + + /** + * Forward or reverse sort, but the fallback is always forward + */ + private static abstract class Sort implements Comparator, Serializable { + + private final boolean _rev; + + public Sort(boolean rev) { + _rev = rev; + } + + public int compare(Snark l, Snark r) { + int rv = compareIt(l, r); + if (rv != 0) + return _rev ? 0 - rv : rv; + return TorrentNameComparator.comp(l, r); + } + + protected abstract int compareIt(Snark l, Snark r); + + protected static int compLong(long l, long r) { + if (l < r) + return -1; + if (l > r) + return 1; + return 0; + } + } + + + private static class StatusComparator extends Sort { + + private StatusComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return getStatus(l) - getStatus(r); + } + + private static int getStatus(Snark snark) { + long remaining = snark.getRemainingLength(); + long needed = snark.getNeededLength(); + if (snark.isStopped()) { + if (remaining < 0) + return 0; + if (remaining > 0) + return 5; + return 10; + } + if (snark.isStarting()) + return 15; + if (snark.isAllocating()) + return 20; + if (remaining < 0) + return 15; // magnet + if (remaining == 0) + return 100; + if (snark.isChecking()) + return 95; + if (snark.getNeededLength() <= 0) + return 90; + if (snark.getPeerCount() <= 0) + return 40; + if (snark.getDownloadRate() <= 0) + return 50; + return 60; + } + } + + private static class PeersComparator extends Sort { + + public PeersComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return l.getPeerCount() - r.getPeerCount(); + } + } + + private static class RemainingComparator extends Sort { + + public RemainingComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getNeededLength(), r.getNeededLength()); + } + } + + private static class ETAComparator extends Sort { + + public ETAComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(eta(l), eta(r)); + } + + private static long eta(Snark snark) { + long needed = snark.getNeededLength(); + long total = snark.getTotalLength(); + if (needed > total) + needed = total; + long remainingSeconds; + long downBps = snark.getDownloadRate(); + if (downBps > 0 && needed > 0) + return needed / downBps; + return -1; + } + } + + private static class SizeComparator extends Sort { + + public SizeComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getTotalLength(), r.getTotalLength()); + } + } + private static class DownloadedComparator extends Sort { + + public DownloadedComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getDownloaded(), r.getDownloaded()); + } + } + + private static class UploadedComparator extends Sort { + + public UploadedComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getUploaded(), r.getUploaded()); + } + } + + private static class DownRateComparator extends Sort { + + public DownRateComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getDownloadRate(), r.getDownloadRate()); + } + } + + private static class UpRateComparator extends Sort { + + public UpRateComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getUploadRate(), r.getUploadRate()); + } + } +}