package org.klomp.snark.web;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.i2p.CoreVersion;
import net.i2p.data.Base32;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.util.Log;
import net.i2p.util.SecureFile;
import net.i2p.util.SystemVersion;
import net.i2p.util.Translate;
import org.klomp.snark.I2PSnarkUtil;
import org.klomp.snark.MagnetURI;
import org.klomp.snark.MetaInfo;
import org.klomp.snark.Peer;
import org.klomp.snark.PeerID;
import org.klomp.snark.Snark;
import org.klomp.snark.SnarkManager;
import org.klomp.snark.Storage;
import org.klomp.snark.Tracker;
import org.klomp.snark.TrackerClient;
import org.klomp.snark.dht.DHT;
import org.klomp.snark.standalone.ConfigUIHelper;
/**
* Refactored to eliminate Jetty dependencies.
*/
public class I2PSnarkServlet extends BasicServlet {
private static final long serialVersionUID = 1L;
/** generally "/i2psnark" */
private String _contextPath;
/** generally "i2psnark" */
private String _contextName;
private transient SnarkManager _manager;
private long _nonce;
private String _themePath;
private String _imgPath;
private String _lastAnnounceURL;
private static final String DEFAULT_NAME = "i2psnark";
public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
private static final String WARBASE = "/.resources/";
private static final char HELLIP = '\u2026';
public I2PSnarkServlet() {
super();
}
@Override
public void init(ServletConfig cfg) throws ServletException {
super.init(cfg);
String cpath = getServletContext().getContextPath();
_contextPath = cpath == "" ? "/" : cpath;
_contextName = cpath == "" ? DEFAULT_NAME : cpath.substring(1).replace("/", "_");
_nonce = _context.random().nextLong();
// limited protection against overwriting other config files or directories
// in case you named your war "router.war"
// We don't handle bad characters in the context path. Don't do that.
String configName = _contextName;
if (!configName.equals(DEFAULT_NAME))
configName = DEFAULT_NAME + '_' + _contextName;
_manager = new SnarkManager(_context, _contextPath, configName);
String configFile = _context.getProperty(PROP_CONFIG_FILE);
if ( (configFile == null) || (configFile.trim().length() <= 0) )
configFile = configName + ".config";
_manager.loadConfig(configFile);
_manager.start();
loadMimeMap("org/klomp/snark/web/mime");
setResourceBase(_manager.getDataDir());
setWarBase(WARBASE);
}
@Override
public void destroy() {
if (_manager != null)
_manager.stop();
super.destroy();
}
/**
* We override this to set the file relative to the storage dirctory
* for the torrent.
*
* @param pathInContext should always start with /
*/
@Override
public File getResource(String pathInContext)
{
if (pathInContext == null || pathInContext.equals("/") || pathInContext.equals("/index.jsp") ||
!pathInContext.startsWith("/") || pathInContext.length() == 0 ||
pathInContext.equals("/index.html") || pathInContext.startsWith(WARBASE))
return super.getResource(pathInContext);
// files in the i2psnark/ directory
// get top level
pathInContext = pathInContext.substring(1);
File top = new File(pathInContext);
File parent;
while ((parent = top.getParentFile()) != null) {
top = parent;
}
Snark snark = _manager.getTorrentByBaseName(top.getPath());
if (snark != null) {
Storage storage = snark.getStorage();
if (storage != null) {
File sbase = storage.getBase();
String child = pathInContext.substring(top.getPath().length());
return new File(sbase, child);
}
}
return new File(_resourceBase, pathInContext);
}
/**
* Handle what we can here, calling super.doGet() for the rest.
* @since 0.8.3
*/
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGetAndPost(request, response);
}
/**
* Handle what we can here, calling super.doPost() for the rest.
* @since Jetty 7
*/
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGetAndPost(request, response);
}
/**
* Handle what we can here, calling super.doGet() or super.doPost() for the rest.
*
* Some parts modified from:
* <pre>
// ========================================================================
// $Id: Default.java,v 1.51 2006/10/08 14:13:18 gregwilkins Exp $
// Copyright 199-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================
* </pre>
*
*/
private void doGetAndPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Service " + req.getMethod() + " \"" + req.getContextPath() + "\" \"" + req.getServletPath() + "\" \"" + req.getPathInfo() + '"');
// since we are not overriding handle*(), do this here
String method = req.getMethod();
// this is the part after /i2psnark
String path = req.getServletPath();
// in-war icons etc.
if (path != null && path.startsWith(WARBASE)) {
if (method.equals("GET") || method.equals("HEAD"))
super.doGet(req, resp);
else // no POST either
resp.sendError(405);
return;
}
if (_context.isRouterContext())
_themePath = "/themes/snark/" + _manager.getTheme() + '/';
else
_themePath = _contextPath + WARBASE + "themes/snark/" + _manager.getTheme() + '/';
_imgPath = _themePath + "images/";
req.setCharacterEncoding("UTF-8");
String pOverride = _manager.util().connected() ? null : "";
String peerString = getQueryString(req, pOverride, null, null);
// AJAX for mainsection
if ("/.ajax/xhr1.html".equals(path)) {
setHTMLHeaders(resp);
PrintWriter out = resp.getWriter();
//if (_log.shouldLog(Log.DEBUG))
// _manager.addMessage((_context.clock().now() / 1000) + " xhr1 p=" + req.getParameter("p"));
writeMessages(out, false, peerString);
writeTorrents(out, req);
return;
}
boolean isConfigure = "/configure".equals(path);
// index.jsp doesn't work, it is grabbed by the war handler before here
if (!(path == null || path.equals("/") || path.equals("/index.jsp") ||
path.equals("/index.html") || path.equals("/_post") || isConfigure)) {
if (path.endsWith("/")) {
// Listing of a torrent (torrent detail page)
// bypass the horrid Resource.getListHTML()
String pathInfo = req.getPathInfo();
String pathInContext = addPaths(path, pathInfo);
File resource = getResource(pathInContext);
if (resource == null) {
resp.sendError(404);
} else {
String base = addPaths(req.getRequestURI(), "/");
String listing = getListHTML(resource, base, true, method.equals("POST") ? req.getParameterMap() : null,
req.getParameter("sort"));
if (method.equals("POST")) {
// P-R-G
sendRedirect(req, resp, "");
} else if (listing != null) {
setHTMLHeaders(resp);
resp.getWriter().write(listing);
} else { // shouldn't happen
resp.sendError(404);
}
}
} else {
// local completed files in torrent directories
if (method.equals("GET") || method.equals("HEAD"))
super.doGet(req, resp);
else if (method.equals("POST"))
super.doPost(req, resp);
else
resp.sendError(405);
}
return;
}
// Either the main page or /configure
String nonce = req.getParameter("nonce");
if (nonce != null) {
if (nonce.equals(String.valueOf(_nonce)))
processRequest(req);
else // nonce is constant, shouldn't happen
_manager.addMessage("Please retry form submission (bad nonce)");
// P-R-G (or G-R-G to hide the params from the address bar)
sendRedirect(req, resp, peerString);
return;
}
setHTMLHeaders(resp);
PrintWriter out = resp.getWriter();
out.write(DOCTYPE + "<html>\n" +
"<head><link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">\n" +
"<title>");
if (_contextName.equals(DEFAULT_NAME))
out.write(_t("I2PSnark"));
else
out.write(_contextName);
out.write(" - ");
if (isConfigure)
out.write(_t("Configuration"));
else
out.write(_t("Anonymous BitTorrent Client"));
String peerParam = req.getParameter("p");
if ("2".equals(peerParam))
out.write(" | Debug Mode");
out.write("</title>\n");
// we want it to go to the base URI so we don't refresh with some funky action= value
int delay = 0;
if (!isConfigure) {
delay = _manager.getRefreshDelaySeconds();
if (delay > 0) {
String jsPfx = _context.isRouterContext() ? "" : ".resources";
String downMsg = _context.isRouterContext() ? _t("Router is down") : _t("I2PSnark has stopped");
//out.write("<meta http-equiv=\"refresh\" content=\"" + delay + ";/i2psnark/" + peerString + "\">\n");
out.write("<script src=\"" + jsPfx + "/js/ajax.js\" type=\"text/javascript\"></script>\n" +
"<script type=\"text/javascript\">\n" +
"var failMessage = \"<div class=\\\"routerdown\\\"><b>" + downMsg + "<\\/b><\\/div>\";\n" +
"function requestAjax1() { ajax(\"" + _contextPath + "/.ajax/xhr1.html" +
peerString.replace("&", "&") + // don't html escape in js
"\", \"mainsection\", " + (delay*1000) + "); }\n" +
"function initAjax() { setTimeout(requestAjax1, " + (delay*1000) +"); }\n" +
"</script>\n");
}
}
out.write(HEADER_A + _themePath + HEADER_B + "</head>\n");
if (isConfigure || delay <= 0)
out.write("<body>");
else
out.write("<body onload=\"initAjax()\">");
out.write("<center>");
List<Tracker> sortedTrackers = null;
if (isConfigure) {
out.write("<div class=\"snarknavbar\"><a href=\"" + _contextPath + "/\" title=\"");
out.write(_t("Torrents"));
out.write("\" class=\"snarkRefresh\">");
out.write(toThemeImg("arrow_refresh"));
out.write(" ");
if (_contextName.equals(DEFAULT_NAME))
out.write(_t("I2PSnark"));
else
out.write(_contextName);
out.write("</a>");
} else {
out.write("<div class=\"snarknavbar\"><a href=\"" + _contextPath + '/' + peerString + "\" title=\"");
out.write(_t("Refresh page"));
out.write("\" class=\"snarkRefresh\">");
out.write(toThemeImg("arrow_refresh"));
out.write(" ");
if (_contextName.equals(DEFAULT_NAME))
out.write(_t("I2PSnark"));
else
out.write(_contextName);
out.write("</a>\n");
sortedTrackers = _manager.getSortedTrackers();
if (_context.isRouterContext()) {
out.write("<a href=\"http://forum.i2p/viewforum.php?f=21\" class=\"snarkRefresh\" target=\"_blank\">");
out.write(_t("Forum"));
out.write("</a>\n");
for (Tracker t : sortedTrackers) {
if (t.baseURL == null || !t.baseURL.startsWith("http"))
continue;
if (_manager.util().isKnownOpenTracker(t.announceURL))
continue;
out.write(" <a href=\"" + t.baseURL + "\" class=\"snarkRefresh\" target=\"_blank\">" + t.name + "</a>");
}
}
}
out.write("</div>\n");
String newURL = req.getParameter("newURL");
if (newURL != null && newURL.trim().length() > 0 && req.getMethod().equals("GET"))
_manager.addMessage(_t("Click \"Add torrent\" button to fetch torrent"));
out.write("<div class=\"page\"><div id=\"mainsection\" class=\"mainsection\">");
writeMessages(out, isConfigure, peerString);
if (isConfigure) {
// end of mainsection div
out.write("<div class=\"logshim\"></div></div>\n");
writeConfigForm(out, req);
writeTrackerForm(out, req);
} else {
boolean pageOne = writeTorrents(out, req);
// end of mainsection div
if (pageOne) {
out.write("</div><div id=\"lowersection\">\n");
writeAddForm(out, req);
writeSeedForm(out, req, sortedTrackers);
writeConfigLink(out);
// end of lowersection div
}
out.write("</div>\n");
}
out.write(FOOTER);
}
/**
* The standard HTTP headers for all HTML pages
*
* @since 0.9.16 moved from doGetAndPost()
*/
private static void setHTMLHeaders(HttpServletResponse resp) {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html; charset=UTF-8");
resp.setHeader("Cache-Control", "no-store, max-age=0, no-cache, must-revalidate");
resp.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'");
resp.setDateHeader("Expires", 0);
resp.setHeader("Pragma", "no-cache");
resp.setHeader("X-Frame-Options", "SAMEORIGIN");
resp.setHeader("X-XSS-Protection", "1; mode=block");
resp.setHeader("X-Content-Type-Options", "nosniff");
resp.setHeader("Referrer-Policy", "no-referrer");
}
private void writeMessages(PrintWriter out, boolean isConfigure, String peerString) throws IOException {
List<String> msgs = _manager.getMessages();
if (!msgs.isEmpty()) {
out.write("<div class=\"snarkMessages\">");
out.write("<a href=\"" + _contextPath + '/');
if (isConfigure)
out.write("configure");
if (peerString.length() > 0)
out.write(peerString + "&");
else
out.write("?");
out.write("action=Clear&nonce=" + _nonce + "\">");
String tx = _t("clear messages");
out.write(toThemeImg("delete", tx, tx));
out.write("</a>" +
"<ul>");
for (int i = msgs.size()-1; i >= 0; i--) {
String msg = msgs.get(i);
out.write("<li>" + msg + "</li>\n");
}
out.write("</ul></div>");
}
}
/**
* @return true if on first page
*/
private boolean writeTorrents(PrintWriter out, HttpServletRequest req) throws IOException {
/** dl, ul, down rate, up rate, peers, size */
final long stats[] = {0,0,0,0,0,0};
String peerParam = req.getParameter("p");
String stParam = req.getParameter("st");
List<Snark> snarks = getSortedSnarks(req);
boolean isForm = _manager.util().connected() || !snarks.isEmpty();
if (isForm) {
out.write("<form action=\"_post\" method=\"POST\">\n");
writeHiddenInputs(out, req, null);
}
out.write(TABLE_HEADER);
// Opera and text-mode browsers: no and no input type=image values submitted
// Using a unique name fixes Opera, except for the buttons with js confirms, see below
String ua = req.getHeader("User-Agent");
boolean isDegraded = ua != null && (ua.startsWith("Lynx") || ua.startsWith("w3m") ||
ua.startsWith("ELinks") || ua.startsWith("Links") ||
ua.startsWith("Dillo") || ua.startsWith("Emacs-w3m"));
boolean noThinsp = isDegraded || (ua != null && ua.startsWith("Opera"));
// pages
int start = 0;
int total = snarks.size();
if (stParam != null) {
try {
start = Math.max(0, Math.min(total - 1, Integer.parseInt(stParam)));
} catch (NumberFormatException nfe) {}
}
int pageSize = Math.max(_manager.getPageSize(), 5);
String currentSort = req.getParameter("sort");
boolean showSort = total > 1;
out.write("<tr><th>");
String sort = ("2".equals(currentSort)) ? "-2" : "2";
if (showSort) {
out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
out.write("\">");
}
String tx = _t("Status");
out.write(toThemeImg("status", tx,
showSort ? _t("Sort by {0}", tx)
: tx));
if (showSort)
out.write("</a>");
out.write("</th>\n<th>");
if (_manager.util().connected() && !snarks.isEmpty()) {
out.write(" <a href=\"" + _contextPath + '/');
if (peerParam != null) {
// disable peer view
out.write(getQueryString(req, "", null, null));
out.write("\">");
tx = _t("Hide Peers");
out.write(toThemeImg("hidepeers", tx, tx));
} else {
// enable peer view
out.write(getQueryString(req, "1", null, null));
out.write("\">");
tx = _t("Show Peers");
out.write(toThemeImg("showpeers", tx, tx));
}
out.write("</a><br>\n");
}
out.write("</th>\n<th colspan=\"2\" align=\"left\">");
// cycle through sort by name or type
boolean isTypeSort = false;
if (showSort) {
if (currentSort == null || "0".equals(currentSort) || "1".equals(currentSort)) {
sort = "-1";
} else if ("-1".equals(currentSort)) {
sort = "12";
isTypeSort = true;
} else if ("12".equals(currentSort)) {
sort = "-12";
isTypeSort = true;
} else {
sort = "";
}
out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
out.write("\">");
}
tx = _t("Torrent");
out.write(toThemeImg("torrent", tx,
showSort ? _t("Sort by {0}", (isTypeSort ? _t("File type") : tx))
: tx));
if (showSort)
out.write("</a>");
out.write("</th>\n<th align=\"center\">");
if (total > 0 && (start > 0 || total > pageSize)) {
writePageNav(out, req, start, pageSize, total, noThinsp);
}
out.write("</th>\n<th align=\"right\">");
if (_manager.util().connected() && !snarks.isEmpty()) {
if (showSort) {
sort = ("4".equals(currentSort)) ? "-4" : "4";
out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
out.write("\">");
}
// Translators: Please keep short or translate as " "
tx = _t("ETA");
out.write(toThemeImg("eta", tx,
showSort ? _t("Sort by {0}", _t("Estimated time remaining"))
: _t("Estimated time remaining")));
if (showSort)
out.write("</a>");
}
out.write("</th>\n<th align=\"right\">");
// cycle through sort by size or downloaded
boolean isDlSort = false;
if (showSort) {
if ("5".equals(currentSort)) {
sort = "-5";
} else if ("-5".equals(currentSort)) {
sort = "6";
isDlSort = true;
} else if ("6".equals(currentSort)) {
sort = "-6";
isDlSort = true;
} else {
sort = "5";
}
out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
out.write("\">");
}
// Translators: Please keep short or translate as " "
tx = _t("RX");
out.write(toThemeImg("head_rx", tx,
showSort ? _t("Sort by {0}", (isDlSort ? _t("Downloaded") : _t("Size")))
: _t("Downloaded")));
if (showSort)
out.write("</a>");
out.write("</th>\n<th align=\"right\">");
boolean isRatSort = false;
if (!snarks.isEmpty()) {
// cycle through sort by uploaded or ratio
boolean nextRatSort = false;
if (showSort) {
if ("7".equals(currentSort)) {
sort = "-7";
} else if ("-7".equals(currentSort)) {
sort = "11";
nextRatSort = true;
} else if ("11".equals(currentSort)) {
sort = "-11";
nextRatSort = true;
isRatSort = true;
} else if ("-11".equals(currentSort)) {
sort = "7";
isRatSort = true;
} else {
sort = "7";
}
out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
out.write("\">");
}
// Translators: Please keep short or translate as " "
tx = _t("TX");
out.write(toThemeImg("head_tx", tx,
showSort ? _t("Sort by {0}", (nextRatSort ? _t("Upload ratio") : _t("Uploaded")))
: _t("Uploaded")));
if (showSort)
out.write("</a>");
}
out.write("</th>\n<th align=\"right\">");
if (_manager.util().connected() && !snarks.isEmpty()) {
if (showSort) {
sort = ("8".equals(currentSort)) ? "-8" : "8";
out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
out.write("\">");
}
// Translators: Please keep short or translate as " "
tx = _t("RX Rate");
out.write(toThemeImg("head_rxspeed", tx,
showSort ? _t("Sort by {0}", _t("Down Rate"))
: _t("Down Rate")));
if (showSort)
out.write("</a>");
}
out.write("</th>\n<th align=\"right\">");
if (_manager.util().connected() && !snarks.isEmpty()) {
if (showSort) {
sort = ("9".equals(currentSort)) ? "-9" : "9";
out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort));
out.write("\">");
}
// Translators: Please keep short or translate as " "
tx = _t("TX Rate");
out.write(toThemeImg("head_txspeed", tx,
showSort ? _t("Sort by {0}", _t("Up Rate"))
: _t("Up Rate")));
if (showSort)
out.write("</a>");
}
out.write("</th>\n<th align=\"center\">");
if (_manager.isStopping()) {
out.write(" ");
} else if (_manager.util().connected()) {
if (isDegraded)
out.write("<a href=\"" + _contextPath + "/?action=StopAll&nonce=" + _nonce + "\"><img title=\"");
else {
// http://www.onenaught.com/posts/382/firefox-4-change-input-type-image-only-submits-x-and-y-not-name
//out.write("<input type=\"image\" name=\"action\" value=\"StopAll\" title=\"");
out.write("<input type=\"image\" name=\"action_StopAll\" value=\"foo\" title=\"");
}
out.write(_t("Stop all torrents and the I2P tunnel"));
out.write("\" src=\"" + _imgPath + "stop_all.png\" alt=\"");
out.write(_t("Stop All"));
out.write("\">");
if (isDegraded)
out.write("</a>");
for (Snark s : snarks) {
if (s.isStopped()) {
// show startall too
out.write("<br>");
if (isDegraded)
out.write("<a href=\"" + _contextPath + "/?action=StartAll&nonce=" + _nonce + "\"><img title=\"");
else
out.write("<input type=\"image\" name=\"action_StartAll\" value=\"foo\" title=\"");
out.write(_t("Start all stopped torrents"));
out.write("\" src=\"" + _imgPath + "start_all.png\" alt=\"");
out.write(_t("Start All"));
out.write("\">");
if (isDegraded)
out.write("</a>");
break;
}
}
} else if ((!_manager.util().isConnecting()) && !snarks.isEmpty()) {
if (isDegraded)
out.write("<a href=\"" + _contextPath + "/?action=StartAll&nonce=" + _nonce + "\"><img title=\"");
else
out.write("<input type=\"image\" name=\"action_StartAll\" value=\"foo\" title=\"");
out.write(_t("Start all torrents and the I2P tunnel"));
out.write("\" src=\"" + _imgPath + "start_all.png\" alt=\"");
out.write(_t("Start All"));
out.write("\">");
if (isDegraded)
out.write("</a>");
} else {
out.write(" ");
}
out.write("</th></tr>\n");
out.write("</thead>\n");
String uri = _contextPath + '/';
boolean showDebug = "2".equals(peerParam);
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, req, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug, hide, isRatSort);
}
if (total == 0) {
out.write("<tr class=\"snarkTorrentNoneLoaded\">" +
"<td class=\"snarkTorrentNoneLoaded\"" +
" colspan=\"11\"><i>");
out.write(_t("No torrents loaded."));
out.write("</i></td></tr>\n");
} else /** if (snarks.size() > 1) */ {
out.write("<tfoot><tr>\n" +
" <th align=\"left\" colspan=\"6\">");
out.write(" ");
out.write(_t("Totals"));
out.write(": ");
out.write(ngettext("1 torrent", "{0} torrents", total));
out.write(", ");
out.write(DataHelper.formatSize2(stats[5]) + "B");
if (_manager.util().connected() && total > 0) {
out.write(", ");
out.write(ngettext("1 connected peer", "{0} connected peers", (int) stats[4]));
}
DHT dht = _manager.util().getDHT();
if (dht != null) {
int dhts = dht.size();
if (dhts > 0) {
out.write(", ");
out.write(ngettext("1 DHT peer", "{0} DHT peers", dhts));
}
}
String IPString = _manager.util().getOurIPString();
if(!IPString.equals("unknown")) {
// Only truncate if it's an actual dest
out.write("; ");
out.write(_t("Dest"));
out.write(": <tt>");
out.write(IPString.substring(0, 4));
out.write("</tt>");
}
if (dht != null) {
if (showDebug)
out.write(dht.renderStatusHTML());
}
out.write("</th>\n");
if (_manager.util().connected() && total > 0) {
out.write(" <th align=\"right\">" + formatSize(stats[0]) + "</th>\n" +
" <th align=\"right\">" + formatSize(stats[1]) + "</th>\n" +
" <th align=\"right\">" + formatSize(stats[2]) + "ps</th>\n" +
" <th align=\"right\">" + formatSize(stats[3]) + "ps</th>\n" +
" <th></th>");
} else {
out.write("<th colspan=\"5\"></th>");
}
out.write("</tr></tfoot>\n");
}
out.write("</table>");
if (isForm)
out.write("</form>\n");
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 buf 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("<input type=\"hidden\" name=\"nonce\" value=\"")
.append(_nonce).append("\" >\n");
String peerParam = req.getParameter("p");
if (peerParam != null) {
buf.append("<input type=\"hidden\" name=\"p\" value=\"")
.append(DataHelper.stripHTML(peerParam)).append("\" >\n");
}
String stParam = req.getParameter("st");
if (stParam != null) {
buf.append("<input type=\"hidden\" name=\"st\" value=\"")
.append(DataHelper.stripHTML(stParam)).append("\" >\n");
}
String soParam = req.getParameter("sort");
if (soParam != null) {
buf.append("<input type=\"hidden\" name=\"sort\" value=\"")
.append(DataHelper.stripHTML(soParam)).append("\" >\n");
}
if (action != null) {
buf.append("<input type=\"hidden\" name=\"action\" value=\"")
.append(action).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, HttpServletRequest req, int start, int pageSize, int total,
boolean noThinsp) {
// Page nav
if (start > 0) {
// First
out.write("<a href=\"" + _contextPath);
out.write(getQueryString(req, null, "", null));
out.write("\">");
out.write(toThemeImg("control_rewind_blue", _t("First"), _t("First page")));
out.write("</a> ");
int prev = Math.max(0, start - pageSize);
//if (prev > 0) {
if (true) {
// Back
out.write(" <a href=\"" + _contextPath);
String sprev = (prev > 0) ? Integer.toString(prev) : "";
out.write(getQueryString(req, null, sprev, null));
out.write("\">");
out.write(toThemeImg("control_back_blue", _t("Prev"), _t("Previous page")));
out.write("</a> ");
}
} else {
out.write(
"<img alt=\"\" border=\"0\" class=\"disable\" src=\"" +
_imgPath + "control_rewind_blue.png\">" +
" " +
"<img alt=\"\" border=\"0\" class=\"disable\" src=\"" +
_imgPath + "control_back_blue.png\">" +
" ");
}
// Page count
int pages = 1 + ((total - 1) / pageSize);
if (pages == 1 && start > 0)
pages = 2;
if (pages > 1) {
int page;
if (start + pageSize >= total)
page = pages;
else
page = 1 + (start / pageSize);
//out.write(" " + _t("Page {0}", page) + thinsp(noThinsp) + pages + " ");
out.write(" " + page + thinsp(noThinsp) + pages + " ");
}
if (start + pageSize < total) {
int next = start + pageSize;
//if (next + pageSize < total) {
if (true) {
// Next
out.write(" <a href=\"" + _contextPath);
out.write(getQueryString(req, null, Integer.toString(next), null));
out.write("\">");
out.write(toThemeImg("control_play_blue", _t("Next"), _t("Next page")));
out.write("</a> ");
}
// Last
int last = ((total - 1) / pageSize) * pageSize;
out.write(" <a href=\"" + _contextPath);
out.write(getQueryString(req, null, Integer.toString(last), null));
out.write("\">");
out.write(toThemeImg("control_fastforward_blue", _t("Last"), _t("Last page")));
out.write("</a> ");
} else {
out.write(" " +
"<img alt=\"\" border=\"0\" class=\"disable\" src=\"" +
_imgPath + "control_play_blue.png\">" +
" " +
"<img alt=\"\" border=\"0\" class=\"disable\" src=\"" +
_imgPath + "control_fastforward_blue.png\">");
}
}
/**
* Do what they ask, adding messages to _manager.addMessage as necessary
*/
private void processRequest(HttpServletRequest req) {
String action = req.getParameter("action");
if (action == null) {
// http://www.onenaught.com/posts/382/firefox-4-change-input-type-image-only-submits-x-and-y-not-name
@SuppressWarnings("unchecked") // TODO-Java6: Remove cast, return type is correct
Map<String, String[]> params = req.getParameterMap();
for (Object o : params.keySet()) {
String key = (String) o;
if (key.startsWith("action_") && key.endsWith(".x")) {
action = key.substring(0, key.length() - 2).substring(7);
break;
}
}
if (action == null) {
_manager.addMessage("No action specified");
return;
}
}
// sadly, Opera doesn't send value with input type=image, so we have to use GET there
//if (!"POST".equals(req.getMethod())) {
// _manager.addMessage("Action must be with POST");
// return;
//}
if ("Add".equals(action)) {
String newURL = req.getParameter("nofilter_newURL");
/******
// NOTE - newFile currently disabled in HTML form - see below
File f = null;
if ( (newFile != null) && (newFile.trim().length() > 0) )
f = new File(newFile.trim());
if ( (f != null) && (!f.exists()) ) {
_manager.addMessage(_t("Torrent file {0} does not exist", newFile));
}
if ( (f != null) && (f.exists()) ) {
// NOTE - All this is disabled - load from local file disabled
File local = new File(_manager.getDataDir(), f.getName());
String canonical = null;
try {
canonical = local.getCanonicalPath();
if (local.exists()) {
if (_manager.getTorrent(canonical) != null)
_manager.addMessage(_t("Torrent already running: {0}", newFile));
else
_manager.addMessage(_t("Torrent already in the queue: {0}", newFile));
} else {
boolean ok = FileUtil.copy(f.getAbsolutePath(), local.getAbsolutePath(), true);
if (ok) {
_manager.addMessage(_t("Copying torrent to {0}", local.getAbsolutePath()));
_manager.addTorrent(canonical);
} else {
_manager.addMessage(_t("Unable to copy the torrent to {0}", local.getAbsolutePath()) + ' ' + _t("from {0}", f.getAbsolutePath()));
}
}
} catch (IOException ioe) {
_log.warn("hrm: " + local, ioe);
}
} else
*****/
if (newURL != null) {
newURL = newURL.trim();
String newDir = req.getParameter("nofilter_newDir");
File dir = null;
if (newDir != null) {
newDir = newDir.trim();
if (newDir.length() > 0) {
dir = new SecureFile(newDir);
if (!dir.isAbsolute()) {
_manager.addMessage(_t("Data directory must be an absolute path") + ": " + dir);
return;
}
if (!dir.isDirectory() && !dir.mkdirs()) {
_manager.addMessage(_t("Data directory cannot be created") + ": " + dir);
return;
}
Collection<Snark> snarks = _manager.getTorrents();
for (Snark s : snarks) {
Storage storage = s.getStorage();
if (storage == null)
continue;
File sbase = storage.getBase();
if (isParentOf(sbase, dir)) {
_manager.addMessage(_t("Cannot add torrent {0} inside another torrent: {1}",
dir.getAbsolutePath(), sbase));
return;
}
}
}
}
File dd = _manager.getDataDir();
if (!dd.canWrite()) {
_manager.addMessage(_t("No write permissions for data directory") + ": " + dd);
return;
}
if (newURL.startsWith("http://")) {
FetchAndAdd fetch = new FetchAndAdd(_context, _manager, newURL, dir);
_manager.addDownloader(fetch);
} else if (newURL.startsWith(MagnetURI.MAGNET) || newURL.startsWith(MagnetURI.MAGGOT)) {
addMagnet(newURL, dir);
} else if (newURL.length() == 40 && newURL.replaceAll("[a-fA-F0-9]", "").length() == 0) {
// hex
newURL = newURL.toUpperCase(Locale.US);
addMagnet(MagnetURI.MAGNET_FULL + newURL, dir);
} else if (newURL.length() == 32 && newURL.replaceAll("[a-zA-Z2-7]", "").length() == 0) {
// b32
newURL = newURL.toUpperCase(Locale.US);
addMagnet(MagnetURI.MAGNET_FULL + newURL, dir);
} else {
_manager.addMessage(_t("Invalid URL: Must start with \"http://\", \"{0}\", or \"{1}\"",
MagnetURI.MAGNET, MagnetURI.MAGGOT));
}
} else {
// no file or URL specified
}
} else if (action.startsWith("Stop_")) {
String torrent = action.substring(5);
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
for (String name : _manager.listTorrentFiles() ) {
Snark snark = _manager.getTorrent(name);
if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
_manager.stopTorrent(snark, false);
break;
}
}
}
}
} else if (action.startsWith("Start_")) {
String torrent = action.substring(6);
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
_manager.startTorrent(infoHash);
}
}
} else if (action.startsWith("Remove_")) {
String torrent = action.substring(7);
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
for (String name : _manager.listTorrentFiles() ) {
Snark snark = _manager.getTorrent(name);
if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
MetaInfo meta = snark.getMetaInfo();
if (meta == null) {
// magnet - remove and delete are the same thing
// Remove not shown on UI so we shouldn't get here
_manager.deleteMagnet(snark);
_manager.addMessage(_t("Magnet deleted: {0}", name));
return;
}
File f = new File(name);
File dd = _manager.getDataDir();
boolean canDelete = dd.canWrite() || !f.exists();
_manager.stopTorrent(snark, canDelete);
// TODO race here with the DirMonitor, could get re-added
if (f.delete()) {
_manager.addMessage(_t("Torrent file deleted: {0}", f.getAbsolutePath()));
} else if (f.exists()) {
if (!canDelete)
_manager.addMessage(_t("No write permissions for data directory") + ": " + dd);
_manager.addMessage(_t("Torrent file could not be deleted: {0}", f.getAbsolutePath()));
}
break;
}
}
}
}
} else if (action.startsWith("Delete_")) {
String torrent = action.substring(7);
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
for (String name : _manager.listTorrentFiles() ) {
Snark snark = _manager.getTorrent(name);
if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
MetaInfo meta = snark.getMetaInfo();
if (meta == null) {
// magnet - remove and delete are the same thing
_manager.deleteMagnet(snark);
if (snark instanceof FetchAndAdd)
_manager.addMessage(_t("Download deleted: {0}", name));
else
_manager.addMessage(_t("Magnet deleted: {0}", name));
return;
}
File f = new File(name);
File dd = _manager.getDataDir();
boolean canDelete = dd.canWrite() || !f.exists();
_manager.stopTorrent(snark, canDelete);
// TODO race here with the DirMonitor, could get re-added
if (f.delete()) {
_manager.addMessage(_t("Torrent file deleted: {0}", f.getAbsolutePath()));
} else if (f.exists()) {
if (!canDelete)
_manager.addMessage(_t("No write permissions for data directory") + ": " + dd);
_manager.addMessage(_t("Torrent file could not be deleted: {0}", f.getAbsolutePath()));
return;
}
Storage storage = snark.getStorage();
if (storage == null)
break;
List<List<String>> files = meta.getFiles();
if (files == null) { // single file torrent
for (File df : storage.getFiles()) {
// should be only one
if (df.delete())
_manager.addMessage(_t("Data file deleted: {0}", df.getAbsolutePath()));
else if (df.exists())
_manager.addMessage(_t("Data file could not be deleted: {0}", df.getAbsolutePath()));
// else already gone
}
break;
}
// step 1 delete files
for (File df : storage.getFiles()) {
if (df.delete()) {
//_manager.addMessage(_t("Data file deleted: {0}", df.getAbsolutePath()));
} else if (df.exists()) {
_manager.addMessage(_t("Data file could not be deleted: {0}", df.getAbsolutePath()));
// else already gone
}
}
// step 2 delete dirs bottom-up
Set<File> dirs = storage.getDirectories();
if (dirs == null)
break; // directory deleted out from under us
if (_log.shouldLog(Log.INFO))
_log.info("Dirs to delete: " + DataHelper.toString(dirs));
boolean ok = false;
for (File df : dirs) {
if (df.delete()) {
ok = true;
//_manager.addMessage(_t("Data dir deleted: {0}", df.getAbsolutePath()));
} else if (df.exists()) {
ok = false;
_manager.addMessage(_t("Directory could not be deleted: {0}", df.getAbsolutePath()));
if (_log.shouldLog(Log.WARN))
_log.warn("Could not delete dir " + df);
// else already gone
}
}
// step 3 message for base (last one)
if (ok)
_manager.addMessage(_t("Directory deleted: {0}", storage.getBase()));
break;
}
}
}
}
} else if ("Save".equals(action)) {
String dataDir = req.getParameter("nofilter_dataDir");
boolean filesPublic = req.getParameter("filesPublic") != null;
boolean autoStart = req.getParameter("autoStart") != null;
boolean smartSort = req.getParameter("smartSort") != null;
String seedPct = req.getParameter("seedPct");
String eepHost = req.getParameter("eepHost");
String eepPort = req.getParameter("eepPort");
String i2cpHost = req.getParameter("i2cpHost");
String i2cpPort = req.getParameter("i2cpPort");
String i2cpOpts = buildI2CPOpts(req);
String upLimit = req.getParameter("upLimit");
String upBW = req.getParameter("upBW");
String refreshDel = req.getParameter("refreshDelay");
String startupDel = req.getParameter("startupDelay");
String pageSize = req.getParameter("pageSize");
boolean useOpenTrackers = req.getParameter("useOpenTrackers") != null;
boolean useDHT = req.getParameter("useDHT") != null;
//String openTrackers = req.getParameter("openTrackers");
String theme = req.getParameter("theme");
String lang = req.getParameter("lang");
boolean ratings = req.getParameter("ratings") != null;
boolean comments = req.getParameter("comments") != null;
String commentsName = req.getParameter("nofilter_commentsName");
_manager.updateConfig(dataDir, filesPublic, autoStart, smartSort, refreshDel, startupDel, pageSize,
seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts,
upLimit, upBW, useOpenTrackers, useDHT, theme,
lang, ratings, comments, commentsName);
// update servlet
try {
setResourceBase(_manager.getDataDir());
} catch (ServletException se) {}
} else if ("Save2".equals(action)) {
String taction = req.getParameter("taction");
if (taction != null)
processTrackerForm(taction, req);
} else if ("Create".equals(action)) {
String baseData = req.getParameter("nofilter_baseFile");
if (baseData != null && baseData.trim().length() > 0) {
File baseFile = new File(baseData.trim());
if (!baseFile.isAbsolute())
baseFile = new File(_manager.getDataDir(), baseData);
String announceURL = req.getParameter("announceURL");
// make the user add a tracker on the config form now
//String announceURLOther = req.getParameter("announceURLOther");
//if ( (announceURLOther != null) && (announceURLOther.trim().length() > "http://.i2p/announce".length()) )
// announceURL = announceURLOther;
if (baseFile.exists()) {
File dd = _manager.getDataDir();
if (!dd.canWrite()) {
_manager.addMessage(_t("No write permissions for data directory") + ": " + dd);
return;
}
String torrentName = baseFile.getName();
if (torrentName.toLowerCase(Locale.US).endsWith(".torrent")) {
_manager.addMessage(_t("Cannot add a torrent ending in \".torrent\": {0}", baseFile.getAbsolutePath()));
return;
}
Snark snark = _manager.getTorrentByBaseName(torrentName);
if (snark != null) {
_manager.addMessage(_t("Torrent with this name is already running: {0}", torrentName));
return;
}
if (isParentOf(baseFile,_manager.getDataDir()) ||
isParentOf(baseFile, _manager.util().getContext().getBaseDir()) ||
isParentOf(baseFile, _manager.util().getContext().getConfigDir())) {
_manager.addMessage(_t("Cannot add a torrent including an I2P directory: {0}", baseFile.getAbsolutePath()));
return;
}
Collection<Snark> snarks = _manager.getTorrents();
for (Snark s : snarks) {
Storage storage = s.getStorage();
if (storage == null)
continue;
File sbase = storage.getBase();
if (isParentOf(sbase, baseFile)) {
_manager.addMessage(_t("Cannot add torrent {0} inside another torrent: {1}",
baseFile.getAbsolutePath(), sbase));
return;
}
if (isParentOf(baseFile, sbase)) {
_manager.addMessage(_t("Cannot add torrent {0} including another torrent: {1}",
baseFile.getAbsolutePath(), sbase));
return;
}
}
if (announceURL.equals("none"))
announceURL = null;
_lastAnnounceURL = announceURL;
List<String> backupURLs = new ArrayList<String>();
Enumeration<?> e = req.getParameterNames();
while (e.hasMoreElements()) {
Object o = e.nextElement();
if (!(o instanceof String))
continue;
String k = (String) o;
if (k.startsWith("backup_")) {
String url = k.substring(7);
if (!url.equals(announceURL))
backupURLs.add(DataHelper.stripHTML(url));
}
}
List<List<String>> announceList = null;
if (!backupURLs.isEmpty()) {
// BEP 12 - Put primary first, then the others, each as the sole entry in their own list
if (announceURL == null) {
_manager.addMessage(_t("Error - Cannot include alternate trackers without a primary tracker"));
return;
}
backupURLs.add(0, announceURL);
boolean hasPrivate = false;
boolean hasPublic = false;
for (String url : backupURLs) {
if (_manager.getPrivateTrackers().contains(url))
hasPrivate = true;
else
hasPublic = true;
}
if (hasPrivate && hasPublic) {
_manager.addMessage(_t("Error - Cannot mix private and public trackers in a torrent"));
return;
}
announceList = new ArrayList<List<String>>(backupURLs.size());
for (String url : backupURLs) {
announceList.add(Collections.singletonList(url));
}
}
try {
// This may take a long time to check the storage, but since it already exists,
// it shouldn't be THAT bad, so keep it in this thread.
// TODO thread it for big torrents, perhaps a la FetchAndAdd
boolean isPrivate = _manager.getPrivateTrackers().contains(announceURL);
Storage s = new Storage(_manager.util(), baseFile, announceURL, announceList, null, isPrivate, null);
s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
MetaInfo info = s.getMetaInfo();
File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent");
// FIXME is the storage going to stay around thanks to the info reference?
// now add it, but don't automatically start it
boolean ok = _manager.addTorrent(info, s.getBitField(), torrentFile.getAbsolutePath(), baseFile, true);
if (!ok)
return;
_manager.addMessage(_t("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath());
if (announceURL != null && !_manager.util().getOpenTrackers().contains(announceURL))
_manager.addMessage(_t("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
} catch (IOException ioe) {
_manager.addMessage(_t("Error creating a torrent for \"{0}\"", baseFile.getAbsolutePath()) + ": " + ioe);
_log.error("Error creating a torrent", ioe);
}
} else {
_manager.addMessage(_t("Cannot create a torrent for the nonexistent data: {0}", baseFile.getAbsolutePath()));
}
} else {
_manager.addMessage(_t("Error creating torrent - you must enter a file or directory"));
}
} else if ("StopAll".equals(action)) {
_manager.stopAllTorrents(false);
} else if ("StartAll".equals(action)) {
_manager.startAllTorrents();
} else if ("Clear".equals(action)) {
_manager.clearMessages();
} else {
_manager.addMessage("Unknown POST action: \"" + action + '\"');
}
}
/**
* Redirect a POST to a GET (P-R-G), preserving the peer string
* @since 0.9.5
*/
private void sendRedirect(HttpServletRequest req, HttpServletResponse resp, String p) throws IOException {
String url = req.getRequestURL().toString();
StringBuilder buf = new StringBuilder(128);
if (url.endsWith("_post"))
url = url.substring(0, url.length() - 5);
buf.append(url);
if (p.length() > 0)
buf.append(p.replace("&", "&")); // no you don't html escape the redirect header
resp.setHeader("Location", buf.toString());
resp.sendError(302, "Moved");
}
/** @since 0.9 */
private void processTrackerForm(String action, HttpServletRequest req) {
if (action.equals(_t("Delete selected")) || action.equals(_t("Save tracker configuration"))) {
boolean changed = false;
Map<String, Tracker> trackers = _manager.getTrackerMap();
List<String> removed = new ArrayList<String>();
List<String> open = new ArrayList<String>();
List<String> priv = new ArrayList<String>();
Enumeration<?> e = req.getParameterNames();
while (e.hasMoreElements()) {
Object o = e.nextElement();
if (!(o instanceof String))
continue;
String k = (String) o;
if (k.startsWith("delete_")) {
k = k.substring(7);
Tracker t;
if ((t = trackers.remove(k)) != null) {
removed.add(t.announceURL);
_manager.addMessage(_t("Removed") + ": " + DataHelper.stripHTML(k));
changed = true;
}
} else if (k.startsWith("ttype_")) {
String val = req.getParameter(k);
k = k.substring(6);
if ("1".equals(val))
open.add(k);
else if ("2".equals(val))
priv.add(k);
}
}
if (changed) {
_manager.saveTrackerMap();
}
open.removeAll(removed);
List<String> oldOpen = new ArrayList<String>(_manager.util().getOpenTrackers());
Collections.sort(oldOpen);
Collections.sort(open);
if (!open.equals(oldOpen))
_manager.saveOpenTrackers(open);
priv.removeAll(removed);
// open trumps private
priv.removeAll(open);
List<String> oldPriv = new ArrayList<String>(_manager.getPrivateTrackers());
Collections.sort(oldPriv);
Collections.sort(priv);
if (!priv.equals(oldPriv))
_manager.savePrivateTrackers(priv);
} else if (action.equals(_t("Add tracker"))) {
String name = req.getParameter("tname");
String hurl = req.getParameter("thurl");
String aurl = req.getParameter("taurl");
if (name != null && hurl != null && aurl != null) {
name = DataHelper.stripHTML(name.trim());
hurl = DataHelper.stripHTML(hurl.trim());
aurl = DataHelper.stripHTML(aurl.trim()).replace("=", "=");
if (name.length() > 0 && hurl.startsWith("http://") && TrackerClient.isValidAnnounce(aurl)) {
Map<String, Tracker> trackers = _manager.getTrackerMap();
trackers.put(name, new Tracker(name, aurl, hurl));
_manager.saveTrackerMap();
String type = req.getParameter("add_tracker_type");
if ("1".equals(type)) {
List<String> newOpen = new ArrayList<String>(_manager.util().getOpenTrackers());
newOpen.add(aurl);
_manager.saveOpenTrackers(newOpen);
} else if ("2".equals(type)) {
List<String> newPriv = new ArrayList<String>(_manager.getPrivateTrackers());
newPriv.add(aurl);
_manager.savePrivateTrackers(newPriv);
}
} else {
_manager.addMessage(_t("Enter valid tracker name and URLs"));
}
} else {
_manager.addMessage(_t("Enter valid tracker name and URLs"));
}
} else if (action.equals(_t("Restore defaults"))) {
_manager.setDefaultTrackerMap();
_manager.saveOpenTrackers(null);
_manager.addMessage(_t("Restored default trackers"));
} else {
_manager.addMessage("Unknown POST action: \"" + action + '\"');
}
}
private static final String iopts[] = {"inbound.length", "inbound.quantity",
"outbound.length", "outbound.quantity" };
/** put the individual i2cp selections into the option string */
private static String buildI2CPOpts(HttpServletRequest req) {
StringBuilder buf = new StringBuilder(128);
String p = req.getParameter("i2cpOpts");
if (p != null)
buf.append(p);
for (int i = 0; i < iopts.length; i++) {
p = req.getParameter(iopts[i]);
if (p != null)
buf.append(' ').append(iopts[i]).append('=').append(p);
}
return buf.toString();
}
private List<Snark> getSortedSnarks(HttpServletRequest req) {
ArrayList<Snark> rv = new ArrayList<Snark>(_manager.getTorrents());
if (rv.size() > 1) {
int sort = 0;
String ssort = req.getParameter("sort");
if (ssort != null) {
try {
sort = Integer.parseInt(ssort);
} catch (NumberFormatException nfe) {}
}
if (_manager.isSmartSortEnabled())
Sorters.setPattern(Translate.getLanguage(_manager.util().getContext()));
else
Sorters.setPattern(null);
try {
Collections.sort(rv, Sorters.getComparator(sort, this));
} catch (IllegalArgumentException iae) {
// Java 7 TimSort - may be unstable
}
}
return rv;
}
private static final int MAX_DISPLAYED_FILENAME_LENGTH = 50;
private static final int MAX_DISPLAYED_ERROR_LENGTH = 43;
/**
* Display one snark (one line in table, unless showPeers is true)
*
* @param stats in/out param (totals)
* @param statsOnly if true, output nothing, update stats only
*/
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,
boolean showRatios) throws IOException {
// stats
long uploaded = snark.getUploaded();
stats[0] += snark.getDownloaded();
stats[1] += uploaded;
long downBps = snark.getDownloadRate();
long upBps = snark.getUploadRate();
boolean isRunning = !snark.isStopped();
if (isRunning) {
stats[2] += downBps;
stats[3] += upBps;
}
int curPeers = snark.getPeerCount();
stats[4] += curPeers;
long total = snark.getTotalLength();
if (total > 0)
stats[5] += total;
if (statsOnly)
return;
String basename = snark.getBaseName();
String fullBasename = basename;
if (basename.length() > MAX_DISPLAYED_FILENAME_LENGTH) {
String start = basename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH);
if (start.indexOf(' ') < 0 && start.indexOf('-') < 0) {
// browser has nowhere to break it
basename = start + HELLIP;
}
}
// includes skipped files, -1 for magnet mode
long remaining = snark.getRemainingLength();
if (remaining > total)
remaining = total;
// does not include skipped files, -1 for magnet mode or when not running.
long needed = snark.getNeededLength();
if (needed > total)
needed = total;
long remainingSeconds;
if (downBps > 0 && needed > 0)
remainingSeconds = needed / downBps;
else
remainingSeconds = -1;
MetaInfo meta = snark.getMetaInfo();
String b64 = Base64.encode(snark.getInfoHash());
String b64Short = b64.substring(0, 6);
// isValid means isNotMagnet
boolean isValid = meta != null;
boolean isMultiFile = isValid && meta.getFiles() != null;
String err = snark.getTrackerProblems();
int knownPeers = Math.max(curPeers, snark.getTrackerSeenPeers());
String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
String statusString;
if (snark.isChecking()) {
statusString = toThemeImg("stalled", "", _t("Checking")) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("Checking") + ' ' +
(new DecimalFormat("0.00%")).format(snark.getCheckingProgress());
} else if (snark.isAllocating()) {
statusString = toThemeImg("stalled", "", _t("Allocating")) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("Allocating");
} else if (err != null && isRunning && curPeers == 0) {
//} else if (err != null && curPeers == 0) {
// Also don't show if seeding... but then we won't see the not-registered error
// && remaining != 0 && needed != 0) {
// let's only show this if we have no peers, otherwise PEX and DHT should bail us out, user doesn't care
//if (isRunning && curPeers > 0 && !showPeers)
// statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td>" +
// "<td class=\"snarkTorrentStatus " + rowClass + "\">" + _t("Tracker Error") +
// ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" +
// curPeers + thinsp(noThinsp) +
// ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
//else if (isRunning)
//if (isRunning) {
statusString = toThemeImg("trackererror", "", err) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("Tracker Error") +
": " + curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers);
//} else {
// if (err.length() > MAX_DISPLAYED_ERROR_LENGTH)
// err = DataHelper.escapeHTML(err.substring(0, MAX_DISPLAYED_ERROR_LENGTH)) + "…";
// else
// err = DataHelper.escapeHTML(err);
// statusString = toThemeImg("trackererror", "", err) + "</td>" +
// "<td class=\"snarkTorrentStatus\">" + _t("Tracker Error");
//}
} else if (snark.isStarting()) {
statusString = toThemeImg("stalled", "", _t("Starting")) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("Starting");
} else if (remaining == 0 || needed == 0) { // < 0 means no meta size yet
// partial complete or seeding
if (isRunning) {
String img;
String txt;
if (remaining == 0) {
img = "seeding";
txt = _t("Seeding");
} else {
// partial
img = "complete";
txt = _t("Complete");
}
if (curPeers > 0 && !showPeers) {
statusString = toThemeImg(img, "", txt) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + txt +
": <a href=\"" + uri + getQueryString(req, b64, null, null) + '#' + b64Short + "\">" +
curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
} else {
statusString = toThemeImg(img, "", txt) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + txt +
": " + curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers);
}
} else {
statusString = toThemeImg("complete", "", _t("Complete")) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("Complete");
}
} else {
if (isRunning && curPeers > 0 && downBps > 0 && !showPeers) {
statusString = toThemeImg("downloading", "", _t("OK")) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("OK") +
": <a href=\"" + uri + getQueryString(req, b64, null, null) + '#' + b64Short + "\">" +
curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
} else if (isRunning && curPeers > 0 && downBps > 0) {
statusString = toThemeImg("downloading", "", _t("OK")) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("OK") +
": " + curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers);
} else if (isRunning && curPeers > 0 && !showPeers) {
statusString = toThemeImg("stalled", "", _t("Stalled")) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("Stalled") +
": <a href=\"" + uri + getQueryString(req, b64, null, null) + '#' + b64Short + "\">" +
curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
} else if (isRunning && curPeers > 0) {
statusString = toThemeImg("stalled", "", _t("Stalled")) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("Stalled") +
": " + curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers);
} else if (isRunning && knownPeers > 0) {
statusString = toThemeImg("nopeers", "", _t("No Peers")) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("No Peers") +
": 0" + thinsp(noThinsp) + knownPeers ;
} else if (isRunning) {
statusString = toThemeImg("nopeers", "", _t("No Peers")) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("No Peers");
} else {
statusString = toThemeImg("stopped", "", _t("Stopped")) + "</td>" +
"<td class=\"snarkTorrentStatus\">" + _t("Stopped");
}
}
out.write("<tr class=\"" + rowClass + "\" id=\"" + b64Short + "\">");
out.write("<td class=\"center\">");
out.write(statusString + "</td>\n\t");
// (i) icon column
out.write("<td>");
if (isValid) {
String announce = meta.getAnnounce();
if (announce == null)
announce = snark.getTrackerURL();
if (announce != null) {
// Link to tracker details page
String trackerLink = getTrackerLink(announce, snark.getInfoHash());
if (trackerLink != null)
out.write(trackerLink);
}
}
String encodedBaseName = encodePath(fullBasename);
// File type icon column
out.write("</td>\n<td>");
if (isValid) {
// Link to local details page - note that trailing slash on a single-file torrent
// gets us to the details page instead of the file.
StringBuilder buf = new StringBuilder(128);
buf.append("<a href=\"").append(encodedBaseName)
.append("/\" title=\"").append(_t("Torrent details"))
.append("\">");
out.write(buf.toString());
}
String icon;
if (isMultiFile)
icon = "folder";
else if (isValid)
icon = toIcon(meta.getName());
else if (snark instanceof FetchAndAdd)
icon = "basket_put";
else
icon = "magnet";
if (isValid) {
out.write(toImg(icon));
out.write("</a>");
} else {
out.write(toImg(icon));
}
// Torrent name column
out.write("</td><td class=\"snarkTorrentName\"");
if (isMultiFile) {
// link on the whole td
out.write(" onclick=\"document.location='" + encodedBaseName + "/';\">");
} else {
out.write('>');
}
if (remaining == 0 || isMultiFile) {
StringBuilder buf = new StringBuilder(128);
buf.append("<a href=\"").append(encodedBaseName);
if (isMultiFile)
buf.append('/');
buf.append("\" title=\"");
if (isMultiFile)
buf.append(_t("View files"));
else
buf.append(_t("Open file"));
buf.append("\">");
out.write(buf.toString());
}
out.write(DataHelper.escapeHTML(basename));
if (remaining == 0 || isMultiFile)
out.write("</a>");
out.write("<td align=\"right\" class=\"snarkTorrentETA\">");
if(isRunning && remainingSeconds > 0 && !snark.isChecking())
out.write(DataHelper.formatDuration2(Math.max(remainingSeconds, 10) * 1000)); // (eta 6h)
out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentDownloaded\">");
if (remaining > 0)
out.write(formatSize(total-remaining) + thinsp(noThinsp) + formatSize(total));
else if (remaining == 0)
out.write(formatSize(total)); // 3GB
//else
// out.write("??"); // no meta size yet
out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentUploaded\">");
if (isValid) {
if (showRatios) {
if (total > 0) {
double ratio = uploaded / ((double) total);
out.write((new DecimalFormat("0.000")).format(ratio));
out.write(" x");
}
} else if (uploaded > 0) {
out.write(formatSize(uploaded));
}
}
out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">");
if (isRunning && needed > 0)
out.write(formatSize(downBps) + "ps");
out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentRateUp\">");
if (isRunning && isValid)
out.write(formatSize(upBps) + "ps");
out.write("</td>\n\t");
out.write("<td align=\"center\" class=\"snarkTorrentAction\">");
if (snark.isChecking()) {
// show no buttons
} else if (isRunning) {
// Stop Button
if (isDegraded)
out.write("<a href=\"" + _contextPath + "/?action=Stop_" + b64 + "&nonce=" + _nonce +
getQueryString(req, "", null, null).replace("?", "&") + "\"><img title=\"");
else
out.write("<input type=\"image\" name=\"action_Stop_" + b64 + "\" value=\"foo\" title=\"");
out.write(_t("Stop the torrent"));
out.write("\" src=\"" + _imgPath + "stop.png\" alt=\"");
out.write(_t("Stop"));
out.write("\">");
if (isDegraded)
out.write("</a>");
} else if (!snark.isStarting()) {
if (!_manager.isStopping()) {
// Start Button
// This works in Opera but it's displayed a little differently, so use noThinsp here too so all 3 icons are consistent
if (noThinsp)
out.write("<a href=\"" + _contextPath + "/?action=Start_" + b64 + "&nonce=" + _nonce +
getQueryString(req, "", null, null).replace("?", "&") + "\"><img title=\"");
else
out.write("<input type=\"image\" name=\"action_Start_" + b64 + "\" value=\"foo\" title=\"");
out.write(_t("Start the torrent"));
out.write("\" src=\"" + _imgPath + "start.png\" alt=\"");
out.write(_t("Start"));
out.write("\">");
if (isDegraded)
out.write("</a>");
}
if (isValid) {
// Remove Button
// Doesnt work with Opera so use noThinsp instead of isDegraded
if (noThinsp)
out.write("<a href=\"" + _contextPath + "/?action=Remove_" + b64 + "&nonce=" + _nonce +
getQueryString(req, "", null, null).replace("?", "&") + "\"><img title=\"");
else
out.write("<input type=\"image\" name=\"action_Remove_" + b64 + "\" value=\"foo\" title=\"");
out.write(_t("Remove the torrent from the active list, deleting the .torrent file"));
out.write("\" onclick=\"if (!confirm('");
// Can't figure out how to escape double quotes inside the onclick string.
// Single quotes in translate strings with parameters must be doubled.
// Then the remaining single quote must be escaped
out.write(_t("Are you sure you want to delete the file \\''{0}\\'' (downloaded data will not be deleted) ?",
escapeJSString(snark.getName())));
out.write("')) { return false; }\"");
out.write(" src=\"" + _imgPath + "remove.png\" alt=\"");
out.write(_t("Remove"));
out.write("\">");
if (isDegraded)
out.write("</a>");
}
// Delete Button
// Doesnt work with Opera so use noThinsp instead of isDegraded
if (noThinsp)
out.write("<a href=\"" + _contextPath + "/?action=Delete_" + b64 + "&nonce=" + _nonce +
getQueryString(req, "", null, null).replace("?", "&") + "\"><img title=\"");
else
out.write("<input type=\"image\" name=\"action_Delete_" + b64 + "\" value=\"foo\" title=\"");
out.write(_t("Delete the .torrent file and the associated data file(s)"));
out.write("\" onclick=\"if (!confirm('");
// Can't figure out how to escape double quotes inside the onclick string.
// Single quotes in translate strings with parameters must be doubled.
// Then the remaining single quote must be escaped
out.write(_t("Are you sure you want to delete the torrent \\''{0}\\'' and all downloaded data?",
escapeJSString(fullBasename)));
out.write("')) { return false; }\"");
out.write(" src=\"" + _imgPath + "delete.png\" alt=\"");
out.write(_t("Delete"));
out.write("\">");
if (isDegraded)
out.write("</a>");
}
out.write("</td>\n</tr>\n");
if(showPeers && isRunning && curPeers > 0) {
List<Peer> peers = snark.getPeerList();
if (!showDebug)
Collections.sort(peers, new PeerComparator());
for (Peer peer : peers) {
if (!peer.isConnected())
continue;
out.write("<tr class=\"" + rowClass + "\"><td></td>");
out.write("<td colspan=\"4\" align=\"right\">");
PeerID pid = peer.getPeerID();
String ch = pid != null ? pid.toString().substring(0, 4) : "????";
String client;
if ("AwMD".equals(ch))
client = _t("I2PSnark");
else if ("BFJT".equals(ch))
client = "I2PRufus";
else if ("TTMt".equals(ch))
client = "I2P-BT";
else if ("LUFa".equals(ch))
client = "Vuze" + getAzVersion(pid.getID());
else if ("CwsL".equals(ch))
client = "I2PSnarkXL";
else if ("LVhE".equals(ch))
client = "XD" + getAzVersion(pid.getID());
else if ("ZV".equals(ch.substring(2,4)) || "VUZP".equals(ch))
client = "Robert" + getRobtVersion(pid.getID());
else if (ch.startsWith("LV")) // LVCS 1.0.2?; LVRS 1.0.4
client = "Transmission" + getAzVersion(pid.getID());
else if ("LUtU".equals(ch))
client = "KTorrent" + getAzVersion(pid.getID());
else
client = _t("Unknown") + " (" + ch + ')';
out.write(client + " <tt>" + peer.toString().substring(5, 9)+ "</tt>");
if (showDebug)
out.write(" inactive " + (peer.getInactiveTime() / 1000) + "s");
out.write("</td>\n\t");
out.write("<td class=\"snarkTorrentStatus\">");
out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentStatus\">");
float pct;
if (isValid) {
pct = (float) (100.0 * peer.completed() / meta.getPieces());
if (pct >= 100.0)
out.write(_t("Seed"));
else {
String ps = String.valueOf(pct);
if (ps.length() > 5)
ps = ps.substring(0, 5);
out.write(ps + "%");
}
} else {
pct = (float) 101.0;
// until we get the metainfo we don't know how many pieces there are
//out.write("??");
}
out.write("</td>\n\t");
out.write("<td class=\"snarkTorrentStatus\">");
out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentStatus\">");
if (needed > 0) {
if (peer.isInteresting() && !peer.isChoked()) {
out.write("<span class=\"unchoked\">");
out.write(formatSize(peer.getDownloadRate()) + "ps</span>");
} else {
out.write("<span class=\"choked\"><a title=\"");
if (!peer.isInteresting())
out.write(_t("Uninteresting (The peer has no pieces we need)"));
else
out.write(_t("Choked (The peer is not allowing us to request pieces)"));
out.write("\">");
out.write(formatSize(peer.getDownloadRate()) + "ps</a></span>");
}
} else if (!isValid) {
//if (peer supports metadata extension) {
out.write("<span class=\"unchoked\">");
out.write(formatSize(peer.getDownloadRate()) + "ps</span>");
//} else {
//}
}
out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentStatus\">");
if (isValid && pct < 100.0) {
if (peer.isInterested() && !peer.isChoking()) {
out.write("<span class=\"unchoked\">");
out.write(formatSize(peer.getUploadRate()) + "ps</span>");
} else {
out.write("<span class=\"choked\"><a title=\"");
if (!peer.isInterested())
out.write(_t("Uninterested (We have no pieces the peer needs)"));
else
out.write(_t("Choking (We are not allowing the peer to request pieces)"));
out.write("\">");
out.write(formatSize(peer.getUploadRate()) + "ps</a></span>");
}
}
out.write("</td>\n\t");
out.write("<td class=\"snarkTorrentStatus\">");
out.write("</td></tr>\n\t");
if (showDebug)
out.write("<tr class=\"" + rowClass + "\"><td></td><td colspan=\"10\" align=\"right\">" + peer.getSocket() + "</td></tr>");
}
}
}
/**
* Make it JS and HTML-safe
* @since 0.9.15
* http://stackoverflow.com/questions/8749001/escaping-html-entities-in-javascript-string-literals-within-the-script-block
*/
private static String escapeJSString(String s) {
return s.replace("\\", "\\u005c")
.replace("<", "\\u003c")
.replace(">", "\\u003e")
.replace("\"", "\\u0022")
.replace("'", "\\u0027")
.replace("&", "\\u0026");
}
/**
* Get version from bytes 3-6
* @return " w.x.y.z" or ""
* @since 0.9.14
*/
private static String getAzVersion(byte[] id) {
if (id[7] != '-')
return "";
StringBuilder buf = new StringBuilder(16);
buf.append(' ');
for (int i = 3; i <= 6; i++) {
int val = id[i] - '0';
if (val < 0)
return "";
if (val > 9)
val = id[i] - 'A';
if (i != 6 || val != 0) {
if (i != 3)
buf.append('.');
buf.append(val);
}
}
return buf.toString();
}
/**
* Get version from bytes 3-5
* @return " w.x.y" or ""
* @since 0.9.14
*/
private static String getRobtVersion(byte[] id) {
StringBuilder buf = new StringBuilder(8);
buf.append(' ');
for (int i = 3; i <= 5; i++) {
int val = id[i];
if (val < 0)
return "";
if (i != 3)
buf.append('.');
buf.append(val);
}
return buf.toString();
}
/** @since 0.8.2 */
private static String thinsp(boolean disable) {
if (disable)
return " / ";
return (" / ");
}
/**
* Sort by completeness (seeds first), then by ID
* @since 0.8.1
*/
private static class PeerComparator implements Comparator<Peer>, Serializable {
public int compare(Peer l, Peer r) {
int diff = r.completed() - l.completed(); // reverse
if (diff != 0)
return diff;
return l.toString().substring(5, 9).compareTo(r.toString().substring(5, 9));
}
}
/**
* Generate link to details page if we know it supports it.
* Start of anchor only, caller must add anchor text or img and close anchor.
*
* @return string or null if unknown tracker
* @since 0.8.4
*/
private String getTrackerLinkUrl(String announce, byte[] infohash) {
// temporarily hardcoded for postman* and anonymity, requires bytemonsoon patch for lookup by info_hash
if (announce != null && (announce.startsWith("http://YRgrgTLG") || announce.startsWith("http://8EoJZIKr") ||
announce.startsWith("http://lnQ6yoBT") || announce.startsWith("http://tracker2.postman.i2p/") ||
announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/"))) {
for (Tracker t : _manager.getTrackers()) {
String aURL = t.announceURL;
if (!(aURL.startsWith(announce) || // vvv hack for non-b64 announce in list vvv
(announce.startsWith("http://lnQ6yoBT") && aURL.startsWith("http://tracker2.postman.i2p/")) ||
(announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/") && aURL.startsWith("http://tracker2.postman.i2p/"))))
continue;
String baseURL = urlEncode(t.baseURL);
String name = DataHelper.escapeHTML(t.name);
StringBuilder buf = new StringBuilder(128);
buf.append("<a href=\"").append(baseURL).append("details.php?dllist=1&filelist=1&info_hash=")
.append(TrackerClient.urlencode(infohash))
.append("\" title=\"").append(_t("Details at {0} tracker", name)).append("\" target=\"_blank\">");
return buf.toString();
}
}
return null;
}
/**
* Full link to details page with img
* @return string or null if details page unsupported
* @since 0.8.4
*/
private String getTrackerLink(String announce, byte[] infohash) {
String linkUrl = getTrackerLinkUrl(announce, infohash);
if (linkUrl != null) {
StringBuilder buf = new StringBuilder(128);
buf.append(linkUrl);
toThemeImg(buf, "details", _t("Info"), "");
buf.append("</a>");
return buf.toString();
}
return null;
}
/**
* Full anchor to home page or details page with shortened host name as anchor text
* @return string, non-null
* @since 0.9.5
*/
private String getShortTrackerLink(String announce, byte[] infohash) {
StringBuilder buf = new StringBuilder(128);
String trackerLinkUrl = getTrackerLinkUrl(announce, infohash);
if (announce.startsWith("http://"))
announce = announce.substring(7);
// strip path
int slsh = announce.indexOf('/');
if (slsh > 0)
announce = announce.substring(0, slsh);
if (trackerLinkUrl != null) {
buf.append(trackerLinkUrl);
} else {
// browsers don't like a full b64 dest, so convert it to b32
String host = announce;
if (host.length() >= 516) {
int colon = announce.indexOf(':');
String port = "";
if (colon > 0) {
port = host.substring(colon);
host = host.substring(0, colon);
}
if (host.endsWith(".i2p"))
host = host.substring(0, host.length() - 4);
byte[] b = Base64.decode(host);
if (b != null) {
Hash h = _context.sha().calculateHash(b);
// should we add the port back or strip it?
host = Base32.encode(h.getData()) + ".b32.i2p" + port;
}
}
buf.append("<a href=\"http://").append(urlEncode(host)).append("/\">");
}
// strip port
int colon = announce.indexOf(':');
if (colon > 0)
announce = announce.substring(0, colon);
if (announce.length() > 67)
announce = DataHelper.escapeHTML(announce.substring(0, 40)) + "…" +
DataHelper.escapeHTML(announce.substring(announce.length() - 8));
buf.append(announce);
buf.append("</a>");
return buf.toString();
}
private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException {
// display incoming parameter if a GET so links will work
String newURL = req.getParameter("nofilter_newURL");
if (newURL == null || newURL.trim().length() <= 0 || req.getMethod().equals("POST"))
newURL = "";
else
newURL = DataHelper.stripHTML(newURL); // XSS
//String newFile = req.getParameter("newFile");
//if ( (newFile == null) || (newFile.trim().length() <= 0) ) newFile = "";
out.write("<div class=\"snarkNewTorrent\">\n");
// *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file
out.write("<form action=\"_post\" method=\"POST\">\n");
writeHiddenInputs(out, req, "Add");
out.write("<div class=\"addtorrentsection\"><span class=\"snarkConfigTitle\">");
out.write(toThemeImg("add"));
out.write(' ');
out.write(_t("Add Torrent"));
out.write("</span><hr>\n<table border=\"0\"><tr><td>");
out.write(_t("From URL"));
out.write(":<td><input type=\"text\" name=\"nofilter_newURL\" size=\"85\" value=\"" + newURL + "\" spellcheck=\"false\"");
out.write(" title=\"");
out.write(_t("Enter the torrent file download URL (I2P only), magnet link, maggot link, or info hash"));
out.write("\"> \n");
// not supporting from file at the moment, since the file name passed isn't always absolute (so it may not resolve)
//out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br>");
out.write("<input type=\"submit\" class=\"add\" value=\"");
out.write(_t("Add torrent"));
out.write("\" name=\"foo\" ><br>\n" +
"<tr><td>");
out.write(_t("Data dir"));
out.write(":<td><input type=\"text\" name=\"nofilter_newDir\" size=\"85\" value=\"\" spellcheck=\"false\"");
out.write(" title=\"");
out.write(_t("Enter the directory to save the data in (default {0})", _manager.getDataDir().getAbsolutePath()));
out.write("\"></td></tr>\n");
out.write("<tr><td> <td><span class=\"snarkAddInfo\">");
out.write(_t("You can also copy .torrent files to: {0}.", "<code>" + _manager.getDataDir().getAbsolutePath() + "</code>"));
out.write("\n");
out.write(_t("Removing a .torrent will cause it to stop."));
out.write("<br></span></table>\n");
out.write("</div></form></div>");
}
private void writeSeedForm(PrintWriter out, HttpServletRequest req, List<Tracker> sortedTrackers) throws IOException {
out.write("<a name=\"add\"></a><div class=\"newtorrentsection\"><div class=\"snarkNewTorrent\">\n");
// *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file
out.write("<form action=\"_post\" method=\"POST\">\n");
writeHiddenInputs(out, req, "Create");
out.write("<span class=\"snarkConfigTitle\">");
out.write(toThemeImg("create"));
out.write(' ');
out.write(_t("Create Torrent"));
out.write("</span><hr>\n<table border=\"0\"><tr><td>");
//out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br>\n");
out.write(_t("Data to seed"));
out.write(":<td>"
+ "<input type=\"text\" name=\"nofilter_baseFile\" size=\"85\" value=\""
+ "\" spellcheck=\"false\" title=\"");
out.write(_t("File or directory to seed (full path or within the directory {0} )",
_manager.getDataDir().getAbsolutePath() + File.separatorChar));
out.write("\" ><tr><td>\n");
out.write(_t("Trackers"));
out.write(":<td><table style=\"width: 30%;\"><tr><td></td><td align=\"center\">");
out.write(_t("Primary"));
out.write("</td><td align=\"center\">");
out.write(_t("Alternates"));
out.write("</td><td rowspan=\"0\">" +
" <input type=\"submit\" class=\"create\" value=\"");
out.write(_t("Create torrent"));
out.write("\" name=\"foo\" >" +
"</td></tr>\n");
for (Tracker t : sortedTrackers) {
String name = t.name;
String announceURL = t.announceURL.replace("=", "=");
out.write("<tr><td>");
out.write(name);
out.write("</td><td align=\"center\"><input type=\"radio\" name=\"announceURL\" value=\"");
out.write(announceURL);
out.write("\"");
if (announceURL.equals(_lastAnnounceURL))
out.write(" checked");
out.write("></td><td align=\"center\"><input type=\"checkbox\" name=\"backup_");
out.write(announceURL);
out.write("\" value=\"foo\"></td></tr>\n");
}
out.write("<tr><td><i>");
out.write(_t("none"));
out.write("</i></td><td align=\"center\"><input type=\"radio\" name=\"announceURL\" value=\"none\"");
if (_lastAnnounceURL == null)
out.write(" checked");
out.write("></td><td></td></tr></table>\n");
// make the user add a tracker on the config form now
//out.write(_t("or"));
//out.write(" <input type=\"text\" name=\"announceURLOther\" size=\"57\" value=\"http://\" " +
// "title=\"");
//out.write(_t("Specify custom tracker announce URL"));
//out.write("\" > " +
out.write("</td></tr>" +
"</table>\n" +
"</form></div></div>");
}
private static final int[] times = { 5, 15, 30, 60, 2*60, 5*60, 10*60, 30*60, -1 };
private void writeConfigForm(PrintWriter out, HttpServletRequest req) throws IOException {
String dataDir = _manager.getDataDir().getAbsolutePath();
boolean filesPublic = _manager.areFilesPublic();
boolean autoStart = _manager.shouldAutoStart();
boolean smartSort = _manager.isSmartSortEnabled();
boolean useOpenTrackers = _manager.util().shouldUseOpenTrackers();
//String openTrackers = _manager.util().getOpenTrackerString();
boolean useDHT = _manager.util().shouldUseDHT();
//int seedPct = 0;
out.write("<form action=\"" + _contextPath + "/configure\" method=\"POST\">\n" +
"<div class=\"configsectionpanel\"><div class=\"snarkConfig\">\n");
writeHiddenInputs(out, req, "Save");
out.write("<span class=\"snarkConfigTitle\">");
out.write(toThemeImg("config"));
out.write(' ');
out.write(_t("Configuration"));
out.write("</span><hr>\n" +
"<table border=\"0\"><tr><td>");
out.write(_t("Data directory"));
out.write(": <td><input name=\"nofilter_dataDir\" size=\"80\" value=\"" +
DataHelper.escapeHTML(dataDir) + "\" spellcheck=\"false\"></td>\n" +
"<tr><td>");
out.write(_t("Files readable by all"));
out.write(": <td><input type=\"checkbox\" class=\"optbox\" name=\"filesPublic\" value=\"true\" "
+ (filesPublic ? "checked " : "")
+ "title=\"");
out.write(_t("If checked, other users may access the downloaded files"));
out.write("\" >" +
"<tr><td>");
out.write(_t("Auto start torrents"));
out.write(": <td><input type=\"checkbox\" class=\"optbox\" name=\"autoStart\" value=\"true\" "
+ (autoStart ? "checked " : "")
+ "title=\"");
out.write(_t("If checked, automatically start torrents that are added"));
out.write("\" >" +
"<tr><td>");
out.write(_t("Smart torrent sorting"));
out.write(": <td><input type=\"checkbox\" class=\"optbox\" name=\"smartSort\" value=\"true\" "
+ (smartSort ? "checked " : "")
+ "title=\"");
out.write(_t("If checked, ignore words such as 'the' when sorting"));
out.write("\" >");
if (!_context.isRouterContext()) {
try {
out.write("<tr><td>");
out.write(_t("Language"));
out.write(": <td>");
// class only in standalone builds
out.write(ConfigUIHelper.getLangSettings(_context));
} catch (Throwable t) {}
}
out.write("<tr><td>");
out.write(_t("Theme"));
out.write(": <td><select name='theme'>");
String theme = _manager.getTheme();
String[] themes = _manager.getThemes();
Arrays.sort(themes);
for(int i = 0; i < themes.length; i++) {
if(themes[i].equals(theme))
out.write("\n<OPTION value=\"" + themes[i] + "\" SELECTED>" + themes[i]);
else
out.write("\n<OPTION value=\"" + themes[i] + "\">" + themes[i]);
}
out.write("</select>\n" +
"<tr><td>");
out.write(_t("Refresh time"));
out.write(": <td><select name=\"refreshDelay\">");
int delay = _manager.getRefreshDelaySeconds();
for (int i = 0; i < times.length; i++) {
out.write("<option value=\"");
out.write(Integer.toString(times[i]));
out.write("\"");
if (times[i] == delay)
out.write(" selected=\"selected\"");
out.write(">");
if (times[i] > 0)
out.write(DataHelper.formatDuration2(times[i] * 1000));
else
out.write(_t("Never"));
out.write("</option>\n");
}
out.write("</select><br>" +
"<tr><td>");
if (_context.isRouterContext()) {
out.write(_t("Startup delay"));
out.write(": <td><input name=\"startupDelay\" size=\"4\" class=\"r\" value=\"" + _manager.util().getStartupDelay() + "\"> ");
out.write(_t("minutes"));
out.write("<br>\n" +
"<tr><td>");
}
out.write(_t("Page size"));
out.write(": <td><input name=\"pageSize\" size=\"4\" maxlength=\"6\" class=\"r\" value=\"" + _manager.getPageSize() + "\"> ");
out.write(_t("torrents"));
out.write("<br>\n");
//Auto add: <input type="checkbox" name="autoAdd" value="true" title="If true, automatically add torrents that are found in the data directory" />
//Auto stop: <input type="checkbox" name="autoStop" value="true" title="If true, automatically stop torrents that are removed from the data directory" />
//out.write("<br>\n");
/*
out.write("Seed percentage: <select name=\"seedPct\" disabled=\"true\" >\n\t");
if (seedPct <= 0)
out.write("<option value=\"0\" selected=\"selected\">Unlimited</option>\n\t");
else
out.write("<option value=\"0\">Unlimited</option>\n\t");
if (seedPct == 100)
out.write("<option value=\"100\" selected=\"selected\">100%</option>\n\t");
else
out.write("<option value=\"100\">100%</option>\n\t");
if (seedPct == 150)
out.write("<option value=\"150\" selected=\"selected\">150%</option>\n\t");
else
out.write("<option value=\"150\">150%</option>\n\t");
out.write("</select><br>\n");
*/
out.write("<tr><td>");
out.write(_t("Total uploader limit"));
out.write(": <td><input type=\"text\" name=\"upLimit\" class=\"r\" value=\""
+ _manager.util().getMaxUploaders() + "\" size=\"4\" maxlength=\"3\" > ");
out.write(_t("peers"));
out.write("<br>\n" +
"<tr><td>");
out.write(_t("Up bandwidth limit"));
out.write(": <td><input type=\"text\" name=\"upBW\" class=\"r\" value=\""
+ _manager.util().getMaxUpBW() + "\" size=\"4\" maxlength=\"4\" > KBps <i>");
out.write(_t("Half available bandwidth recommended."));
if (_context.isRouterContext()) {
out.write(" [<a href=\"/config.jsp\" target=\"blank\">");
out.write(_t("View or change router bandwidth"));
out.write("</a>]</i>");
}
out.write("<br>\n<tr><td>");
out.write(_t("Use open trackers also"));
out.write(": <td><input type=\"checkbox\" class=\"optbox\" name=\"useOpenTrackers\" value=\"true\" "
+ (useOpenTrackers ? "checked " : "")
+ "title=\"");
out.write(_t("If checked, announce torrents to open trackers as well as the tracker listed in the torrent file"));
out.write("\" ></td></tr>\n" +
"<tr><td>");
out.write(_t("Enable DHT"));
out.write(": <td><input type=\"checkbox\" class=\"optbox\" name=\"useDHT\" value=\"true\" "
+ (useDHT ? "checked " : "")
+ "title=\"");
out.write(_t("If checked, use DHT"));
out.write("\" ></td></tr>\n");
// "<tr><td>");
//out.write(_t("Open tracker announce URLs"));
//out.write(": <td><input type=\"text\" name=\"openTrackers\" value=\""
// + openTrackers + "\" size=\"50\" ><br>\n");
//out.write("\n");
//out.write("EepProxy host: <input type=\"text\" name=\"eepHost\" value=\""
// + _manager.util().getEepProxyHost() + "\" size=\"15\" /> ");
//out.write("port: <input type=\"text\" name=\"eepPort\" value=\""
// + _manager.util().getEepProxyPort() + "\" size=\"5\" maxlength=\"5\" /><br>\n");
Map<String, String> options = new TreeMap<String, String>(_manager.util().getI2CPOptions());
out.write("<tr><td>");
out.write(_t("Inbound Settings"));
out.write(":<td>");
out.write(renderOptions(1, 10, SnarkManager.DEFAULT_TUNNEL_QUANTITY,
options.remove("inbound.quantity"), "inbound.quantity", TUNNEL));
out.write(" ");
out.write(renderOptions(0, 4, 3, options.remove("inbound.length"), "inbound.length", HOP));
out.write("<tr><td>");
out.write(_t("Outbound Settings"));
out.write(":<td>");
out.write(renderOptions(1, 10, SnarkManager.DEFAULT_TUNNEL_QUANTITY,
options.remove("outbound.quantity"), "outbound.quantity", TUNNEL));
out.write(" ");
out.write(renderOptions(0, 4, 3, options.remove("outbound.length"), "outbound.length", HOP));
if (!_context.isRouterContext()) {
out.write("<tr><td>");
out.write(_t("I2CP host"));
out.write(": <td><input type=\"text\" name=\"i2cpHost\" value=\""
+ _manager.util().getI2CPHost() + "\" size=\"15\" > " +
"<tr><td>");
out.write(_t("I2CP port"));
out.write(": <td><input type=\"text\" name=\"i2cpPort\" class=\"r\" value=\"" +
+ _manager.util().getI2CPPort() + "\" size=\"5\" maxlength=\"5\" > <br>\n");
}
options.remove(I2PSnarkUtil.PROP_MAX_BW);
// was accidentally in the I2CP options prior to 0.8.9 so it will be in old config files
options.remove(SnarkManager.PROP_OPENTRACKERS);
StringBuilder opts = new StringBuilder(64);
for (Map.Entry<String, String> e : options.entrySet()) {
String key = e.getKey();
String val = e.getValue();
opts.append(key).append('=').append(val).append(' ');
}
out.write("<tr><td>");
out.write(_t("I2CP options"));
out.write(": <td><textarea name=\"i2cpOpts\" cols=\"60\" rows=\"1\" wrap=\"off\" spellcheck=\"false\" >"
+ opts.toString() + "</textarea><br>\n" +
"<tr><td colspan=\"2\"> \n" + // spacer
"<tr><td> <td><input type=\"submit\" class=\"accept\" value=\"");
out.write(_t("Save configuration"));
out.write("\" name=\"foo\" >\n" +
"<tr><td colspan=\"2\"> \n" + // spacer
"</table></div></div></form>");
}
/** @since 0.9 */
private void writeTrackerForm(PrintWriter out, HttpServletRequest req) throws IOException {
StringBuilder buf = new StringBuilder(1024);
buf.append("<form action=\"" + _contextPath + "/configure\" method=\"POST\">\n" +
"<div class=\"configsectionpanel\"><div class=\"snarkConfig\">\n");
writeHiddenInputs(buf, req, "Save2");
buf.append("<span class=\"snarkConfigTitle\">");
toThemeImg(buf, "config");
buf.append(' ');
buf.append(_t("Trackers"));
buf.append("</span><hr>\n" +
"<table class=\"trackerconfig\"><tr><th>")
//.append(_t("Remove"))
.append("</th><th>")
.append(_t("Name"))
.append("</th><th>")
.append(_t("Website URL"))
.append("</th><th>")
.append(_t("Standard"))
.append("</th><th>")
.append(_t("Open"))
.append("</th><th>")
.append(_t("Private"))
.append("</th><th>")
.append(_t("Announce URL"))
.append("</th></tr>\n");
List<String> openTrackers = _manager.util().getOpenTrackers();
List<String> privateTrackers = _manager.getPrivateTrackers();
for (Tracker t : _manager.getSortedTrackers()) {
String name = t.name;
String homeURL = t.baseURL;
String announceURL = t.announceURL.replace("=", "=");
boolean isPrivate = privateTrackers.contains(t.announceURL);
boolean isKnownOpen = _manager.util().isKnownOpenTracker(t.announceURL);
boolean isOpen = isKnownOpen || openTrackers.contains(t.announceURL);
buf.append("<tr><td><input type=\"checkbox\" class=\"optbox\" name=\"delete_")
.append(name).append("\" title=\"").append(_t("Delete")).append("\">" +
"</td><td>").append(name)
.append("</td><td>").append(urlify(homeURL, 35))
.append("</td><td><input type=\"radio\" class=\"optbox\" value=\"0\" name=\"ttype_")
.append(announceURL).append("\"");
if (!(isOpen || isPrivate))
buf.append(" checked=\"checked\"");
else if (isKnownOpen)
buf.append(" disabled=\"disabled\"");
buf.append(">" +
"</td><td><input type=\"radio\" class=\"optbox\" value=\"1\" name=\"ttype_")
.append(announceURL).append("\"");
if (isOpen)
buf.append(" checked=\"checked\"");
else if (t.announceURL.equals("http://diftracker.i2p/announce.php") ||
t.announceURL.equals("http://tracker2.postman.i2p/announce.php"))
buf.append(" disabled=\"disabled\"");
buf.append(">" +
"</td><td><input type=\"radio\" class=\"optbox\" value=\"2\" name=\"ttype_")
.append(announceURL).append("\"");
if (isPrivate) {
buf.append(" checked=\"checked\"");
} else if (isKnownOpen ||
t.announceURL.equals("http://diftracker.i2p/announce.php") ||
t.announceURL.equals("http://tracker2.postman.i2p/announce.php")) {
buf.append(" disabled=\"disabled\"");
}
buf.append(">" +
"</td><td>").append(urlify(announceURL, 35))
.append("</td></tr>\n");
}
buf.append("<tr><td><b>")
.append(_t("Add")).append(":</b></td>" +
"<td><input type=\"text\" class=\"trackername\" name=\"tname\" spellcheck=\"false\"></td>" +
"<td><input type=\"text\" class=\"trackerhome\" name=\"thurl\" spellcheck=\"false\"></td>" +
"<td><input type=\"radio\" class=\"optbox\" value=\"0\" name=\"add_tracker_type\" checked=\"checked\"></td>" +
"<td><input type=\"radio\" class=\"optbox\" value=\"1\" name=\"add_tracker_type\"></td>" +
"<td><input type=\"radio\" class=\"optbox\" value=\"2\" name=\"add_tracker_type\"></td>" +
"<td><input type=\"text\" class=\"trackerannounce\" name=\"taurl\" spellcheck=\"false\"></td></tr>\n" +
"<tr><td colspan=\"7\"> </td></tr>\n" + // spacer
"<tr><td colspan=\"2\"></td><td colspan=\"5\">\n" +
"<input type=\"submit\" name=\"taction\" class=\"default\" value=\"").append(_t("Add tracker")).append("\">\n" +
"<input type=\"submit\" name=\"taction\" class=\"delete\" value=\"").append(_t("Delete selected")).append("\">\n" +
"<input type=\"submit\" name=\"taction\" class=\"add\" value=\"").append(_t("Add tracker")).append("\">\n" +
"<input type=\"submit\" name=\"taction\" class=\"accept\" value=\"").append(_t("Save tracker configuration")).append("\">\n" +
// "<input type=\"reset\" class=\"cancel\" value=\"").append(_t("Cancel")).append("\">\n" +
"<input type=\"submit\" name=\"taction\" class=\"reload\" value=\"").append(_t("Restore defaults")).append("\">\n" +
"</td></tr>" +
"<tr><td colspan=\"7\"> </td></tr>\n" + // spacer
"</table></div></div></form>\n");
out.write(buf.toString());
}
private void writeConfigLink(PrintWriter out) throws IOException {
out.write("<div class=\"configsection\"><span class=\"snarkConfig\">\n" +
"<span class=\"snarkConfigTitle\"><a href=\"configure\">");
out.write(toThemeImg("config"));
out.write(' ');
out.write(_t("Configuration"));
out.write("</a></span></span></div>\n");
}
/**
* @param url in base32 or hex
* @param dataDir null to default to snark data directory
* @since 0.8.4
*/
private void addMagnet(String url, File dataDir) {
try {
MagnetURI magnet = new MagnetURI(_manager.util(), url);
String name = magnet.getName();
byte[] ih = magnet.getInfoHash();
String trackerURL = magnet.getTrackerURL();
_manager.addMagnet(name, ih, trackerURL, true, dataDir);
} catch (IllegalArgumentException iae) {
_manager.addMessage(_t("Invalid magnet URL {0}", url));
}
}
/** copied from ConfigTunnelsHelper */
private static final String HOP = "hop";
private static final String TUNNEL = "tunnel";
/** dummies for translation */
private static final String HOPS = ngettext("1 hop", "{0} hops");
private static final String TUNNELS = ngettext("1 tunnel", "{0} tunnels");
/** prevents the ngettext line below from getting tagged */
private static final String DUMMY0 = "{0} ";
private static final String DUMMY1 = "1 ";
/** modded from ConfigTunnelsHelper @since 0.7.14 */
private String renderOptions(int min, int max, int dflt, String strNow, String selName, String name) {
int now = dflt;
try {
now = Integer.parseInt(strNow);
} catch (Throwable t) {}
StringBuilder buf = new StringBuilder(128);
buf.append("<select name=\"").append(selName).append("\">\n");
for (int i = min; i <= max; i++) {
buf.append("<option value=\"").append(i).append("\" ");
if (i == now)
buf.append("selected=\"selected\" ");
// constants to prevent tagging
buf.append(">").append(ngettext(DUMMY1 + name, DUMMY0 + name + 's', i));
buf.append("</option>\n");
}
buf.append("</select>\n");
return buf.toString();
}
/** translate */
private String _t(String s) {
return _manager.util().getString(s);
}
/** translate */
private String _t(String s, Object o) {
return _manager.util().getString(s, o);
}
/** translate */
private String _t(String s, Object o, Object o2) {
return _manager.util().getString(s, o, o2);
}
/** translate (ngettext) @since 0.7.14 */
private String ngettext(String s, String p, int n) {
return _manager.util().getString(n, s, p);
}
/** dummy for tagging */
private static String ngettext(String s, String p) {
return null;
}
// rounding makes us look faster :)
private static String formatSize(long bytes) {
if (bytes < 5000)
return bytes + " B";
else if (bytes < 5*1024*1024)
return ((bytes + 512)/1024) + " KB";
else if (bytes < 10*1024*1024*1024l)
return ((bytes + 512*1024)/(1024*1024)) + " MB";
else
return ((bytes + 512*1024*1024)/(1024*1024*1024)) + " GB";
}
/**
* This is for a full URL. For a path only, use encodePath().
* @since 0.7.14
*/
static String urlify(String s) {
return urlify(s, 100);
}
/**
* This is for a full URL. For a path only, use encodePath().
* @since 0.9
*/
private static String urlify(String s, int max) {
StringBuilder buf = new StringBuilder(256);
// browsers seem to work without doing this but let's be strict
String link = urlEncode(s);
String display;
if (s.length() <= max)
display = DataHelper.escapeHTML(link);
else
display = DataHelper.escapeHTML(s.substring(0, max)) + "…";
buf.append("<a href=\"").append(link).append("\">").append(display).append("</a>");
return buf.toString();
}
/**
* This is for a full URL. For a path only, use encodePath().
* @since 0.8.13
*/
private static String urlEncode(String s) {
return s.replace(";", "%3B").replace("&", "&").replace(" ", "%20")
.replace("<", "%3C").replace(">", "%3E")
.replace("[", "%5B").replace("]", "%5D");
}
private static final String DOCTYPE = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n";
private static final String HEADER_A = "<link href=\"";
private static final String HEADER_B = "snark.css?" + CoreVersion.VERSION + "\" rel=\"stylesheet\" type=\"text/css\" >";
private static final String TABLE_HEADER = "<table border=\"0\" class=\"snarkTorrents\" width=\"100%\" >\n" +
"<thead>\n";
private static final String FOOTER = "</div></center></body></html>";
/**
* Modded heavily from the Jetty version in Resource.java,
* pass Resource as 1st param
* All the xxxResource constructors are package local so we can't extend them.
*
* <pre>
// ========================================================================
// $Id: Resource.java,v 1.32 2009/05/16 01:53:36 gregwilkins Exp $
// Copyright 1996-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================
* </pre>
*
* Get the resource list as a HTML directory listing.
* @param xxxr The Resource unused
* @param base The encoded base URL
* @param parent True if the parent directory should be included
* @param postParams map of POST parameters or null if not a POST
* @param sortParam may be null
* @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
{
String decodedBase = decodePath(base);
String title = decodedBase;
String cpath = _contextPath + '/';
if (title.startsWith(cpath))
title = title.substring(cpath.length());
// Get the snark associated with this directory
String torrentName;
String pathInTorrent;
int slash = title.indexOf('/');
if (slash > 0) {
torrentName = title.substring(0, slash);
pathInTorrent = title.substring(slash);
} else {
torrentName = title;
pathInTorrent = "/";
}
Snark snark = _manager.getTorrentByBaseName(torrentName);
if (snark != null && postParams != null) {
// caller must P-R-G
String[] val = postParams.get("nonce");
if (val != null) {
String nonce = val[0];
if (String.valueOf(_nonce).equals(nonce)) {
if (postParams.get("savepri") != null) {
savePriorities(snark, postParams);
} else if (postParams.get("stop") != null) {
_manager.stopTorrent(snark, false);
} else if (postParams.get("start") != null) {
_manager.startTorrent(snark);
} else if (postParams.get("recheck") != null) {
_manager.recheckTorrent(snark);
} else {
_manager.addMessage("Unknown command");
}
} else {
_manager.addMessage("Please retry form submission (bad nonce)");
}
}
return null;
}
File r;
if (snark != null) {
Storage storage = snark.getStorage();
if (storage != null) {
File sbase = storage.getBase();
if (pathInTorrent.equals("/"))
r = sbase;
else
r = new File(sbase, pathInTorrent);
} else {
// magnet, dummy
r = new File("");
}
} else {
// dummy
r = new File("");
}
boolean showStopStart = snark != null;
boolean showPriority = snark != null && snark.getStorage() != null && !snark.getStorage().complete() &&
r.isDirectory();
StringBuilder buf=new StringBuilder(4096);
buf.append(DOCTYPE).append("<HTML><HEAD><TITLE>");
if (title.endsWith("/"))
title = title.substring(0, title.length() - 1);
String directory = title;
title = _t("Torrent") + ": " + DataHelper.escapeHTML(title);
buf.append(title);
buf.append("</TITLE>\n").append(HEADER_A).append(_themePath).append(HEADER_B)
.append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">\n");
if (showPriority)
buf.append("<script src=\"").append(_contextPath).append(WARBASE + "js/folder.js\" type=\"text/javascript\"></script>\n");
buf.append("</HEAD><BODY");
if (showPriority)
buf.append(" onload=\"setupbuttons()\"");
buf.append(">\n<center><div class=\"snarknavbar\"><a href=\"").append(_contextPath).append("/\" title=\"Torrents\"");
buf.append(" class=\"snarkRefresh\">");
toThemeImg(buf, "arrow_refresh");
buf.append(" ");
if (_contextName.equals(DEFAULT_NAME))
buf.append(_t("I2PSnark"));
else
buf.append(_contextName);
buf.append("</a></div></center>\n");
if (parent) // always true
buf.append("<div class=\"page\"><div class=\"mainsection\">");
// for stop/start/check
if (showStopStart || showPriority) {
buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n");
buf.append("<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");
}
}
if (snark != null) {
// first table - torrent info
buf.append("<table class=\"snarkTorrentInfo\">\n");
buf.append("<tr><th></th><th><b>")
.append(_t("Torrent"))
.append(":</b> ")
.append(DataHelper.escapeHTML(snark.getBaseName()))
.append("</th></tr>\n");
String fullPath = snark.getName();
String baseName = encodePath((new File(fullPath)).getName());
buf.append("<tr><td>");
toThemeImg(buf, "file");
buf.append("</td><td><b>")
.append(_t("Torrent file"))
.append(":</b> <a href=\"").append(_contextPath).append('/').append(baseName).append("\">")
.append(DataHelper.escapeHTML(fullPath))
.append("</a></td></tr>\n");
if (snark.getStorage() != null) {
buf.append("<tr><td>");
toThemeImg(buf, "file");
buf.append("</td><td><b>")
.append(_t("Data location"))
.append(":</b> ")
.append(DataHelper.escapeHTML(snark.getStorage().getBase().getPath()))
.append("</td></tr>\n");
}
String hex = I2PSnarkUtil.toHex(snark.getInfoHash());
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Info hash"))
.append(":</b> ")
.append(hex.toUpperCase(Locale.US))
.append("</td></tr>\n");
String announce = null;
MetaInfo meta = snark.getMetaInfo();
if (meta != null) {
announce = meta.getAnnounce();
if (announce == null)
announce = snark.getTrackerURL();
if (announce != null) {
announce = DataHelper.stripHTML(announce);
buf.append("<tr><td>");
String trackerLink = getTrackerLink(announce, snark.getInfoHash());
if (trackerLink != null)
buf.append(trackerLink);
else
toThemeImg(buf, "details");
buf.append("</td><td><b>").append(_t("Primary Tracker")).append(":</b> ");
buf.append(getShortTrackerLink(announce, snark.getInfoHash()));
buf.append("</td></tr>");
}
List<List<String>> alist = meta.getAnnounceList();
if (alist != null && !alist.isEmpty()) {
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td valign=\"top\"><b>")
.append(_t("Tracker List")).append(":</b> ");
for (List<String> alist2 : alist) {
buf.append('[');
boolean more = false;
for (String s : alist2) {
if (more)
buf.append(' ');
else
more = true;
buf.append(getShortTrackerLink(DataHelper.stripHTML(s), snark.getInfoHash()));
}
buf.append("] ");
}
buf.append("</td></tr>\n");
}
}
if (meta != null) {
String com = meta.getComment();
if (com != null && com.length() > 0) {
if (com.length() > 1024)
com = com.substring(0, 1024);
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Comment")).append(":</b> ")
.append(DataHelper.stripHTML(com))
.append("</td></tr>\n");
}
long dat = meta.getCreationDate();
SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm");
fmt.setTimeZone(SystemVersion.getSystemTimeZone(_context));
if (dat > 0) {
String date = fmt.format(new Date(dat));
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Created")).append(":</b> ")
.append(date)
.append("</td></tr>\n");
}
String cby = meta.getCreatedBy();
if (cby != null && cby.length() > 0) {
if (cby.length() > 128)
cby = com.substring(0, 128);
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Created By")).append(":</b> ")
.append(DataHelper.stripHTML(cby))
.append("</td></tr>\n");
}
long[] dates = _manager.getSavedAddedAndCompleted(snark);
if (dates[0] > 0) {
String date = fmt.format(new Date(dates[0]));
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Added")).append(":</b> ")
.append(date)
.append("</td></tr>\n");
}
if (dates[1] > 0) {
String date = fmt.format(new Date(dates[1]));
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Completed")).append(":</b> ")
.append(date)
.append("</td></tr>\n");
}
}
if (meta == null || !meta.isPrivate()) {
buf.append("<tr><td><a href=\"")
.append(MagnetURI.MAGNET_FULL).append(hex);
if (announce != null)
buf.append("&tr=").append(announce);
buf.append("\">")
.append(toImg("magnet", _t("Magnet link")))
.append("</a></td><td><b>Magnet:</b> <a href=\"")
.append(MagnetURI.MAGNET_FULL).append(hex);
if (announce != null)
buf.append("&tr=").append(announce);
buf.append("\">")
.append(MagnetURI.MAGNET_FULL).append(hex);
if (announce != null)
buf.append("&tr=").append(announce);
buf.append("</a>")
.append("</td></tr>\n");
} else {
buf.append("<tr><td>");
toThemeImg(buf, "details");
buf.append("</td><td><b>")
.append(_t("Private torrent"))
.append("</td></tr>\n");
}
// We don't have the hash of the torrent file
//buf.append("<tr><td>").append(_t("Maggot link")).append(": <a href=\"").append(MAGGOT).append(hex).append(':').append(hex).append("\">")
// .append(MAGGOT).append(hex).append(':').append(hex).append("</a></td></tr>");
buf.append("<tr><td>");
toThemeImg(buf, "size");
buf.append("</td><td><b>")
.append(_t("Size"))
.append(":</b> ")
.append(formatSize(snark.getTotalLength()));
int pieces = snark.getPieces();
double completion = (pieces - snark.getNeeded()) / (double) pieces;
buf.append(" ");
toThemeImg(buf, "head_rx");
buf.append(" <b>");
if (completion < 1.0)
buf.append(_t("Completion"))
.append(":</b> ")
.append((new DecimalFormat("0.00%")).format(completion));
else
buf.append(_t("Complete")).append("</b>");
// up ratio
buf.append(" ");
toThemeImg(buf, "head_tx");
buf.append(" <b>")
.append(_t("Upload ratio"))
.append(":</b> ");
long uploaded = snark.getUploaded();
if (uploaded > 0) {
double ratio = uploaded / ((double) snark.getTotalLength());
buf.append((new DecimalFormat("0.000")).format(ratio));
buf.append(" x");
} else {
buf.append('0');
}
// not including skipped files, but -1 when not running
long needed = snark.getNeededLength();
if (needed < 0) {
// including skipped files, valid when not running
needed = snark.getRemainingLength();
}
if (needed > 0) {
buf.append(" ");
toThemeImg(buf, "head_rx");
buf.append(" <b>")
.append(_t("Remaining"))
.append(":</b> ")
.append(formatSize(needed));
}
long skipped = snark.getSkippedLength();
if (skipped > 0) {
buf.append(" ");
toThemeImg(buf, "head_rx");
buf.append(" <b>")
.append(_t("Skipped"))
.append(":</b> ")
.append(formatSize(skipped));
}
if (meta != null) {
List<List<String>> files = meta.getFiles();
int fileCount = files != null ? files.size() : 1;
buf.append(" ");
toThemeImg(buf, "file");
buf.append(" <b>")
.append(_t("Files"))
.append(":</b> ")
.append(fileCount);
}
buf.append(" ");
toThemeImg(buf, "file");
buf.append(" <b>")
.append(_t("Pieces"))
.append(":</b> ")
.append(pieces);
buf.append(" ");
toThemeImg(buf, "file");
buf.append(" <b>")
.append(_t("Piece size"))
.append(":</b> ")
.append(formatSize(snark.getPieceLength(0)))
.append("</td></tr>\n");
// buttons
if (showStopStart) {
buf.append("<tr><td></td><td>");
if (snark.isChecking()) {
buf.append("<b>").append(_t("Checking")).append("… ")
.append((new DecimalFormat("0.00%")).format(snark.getCheckingProgress()))
.append(" <a href=\"").append(base).append("\">")
.append(_t("Refresh page for results")).append("</a>");
} else if (snark.isStarting()) {
buf.append("<b>").append(_t("Starting")).append("…</b>");
} else if (snark.isAllocating()) {
buf.append("<b>").append(_t("Allocating")).append("…</b>");
} else {
boolean isRunning = !snark.isStopped();
buf.append("<input type=\"submit\" value=\"");
if (isRunning)
buf.append(_t("Stop")).append("\" name=\"stop\" class=\"stoptorrent\">\n");
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)
buf.append("\" class=\"disabled\" disabled=\"disabled\">\n");
else
buf.append("\" class=\"reload\">\n");
}
buf.append("</td></tr>\n");
}
} else {
// snark == null
// shouldn't happen
buf.append("<tr><th>Not found<br>resource=\"").append(r.toString())
.append("\"<br>base=\"").append(base)
.append("\"<br>torrent=\"").append(torrentName)
.append("\"</th></tr>\n");
}
buf.append("</table>\n");
if (snark != null && !r.exists()) {
// fixup TODO
buf.append("<p>Does not exist<br>resource=\"").append(r.toString())
.append("\"<br>base=\"").append(base)
.append("\"<br>torrent=\"").append(torrentName)
.append("\"</p></div></div></BODY></HTML>");
return buf.toString();
}
File[] ls = null;
if (r.isDirectory()) {
ls = r.listFiles();
} // if r is not a directory, we are only showing torrent info section
if (ls == null) {
// We are only showing the torrent info section
buf.append("</div></div></BODY></HTML>");
return buf.toString();
}
Storage storage = snark != null ? snark.getStorage() : null;
List<Sorters.FileAndIndex> fileList = new ArrayList<Sorters.FileAndIndex>(ls.length);
// precompute remaining for all files for efficiency
long[] remainingArray = (storage != null) ? storage.remaining() : null;
for (int i = 0; i < ls.length; i++) {
fileList.add(new Sorters.FileAndIndex(ls[i], storage, remainingArray));
}
boolean showSort = fileList.size() > 1;
if (showSort) {
int sort = 0;
if (sortParam != null) {
try {
sort = Integer.parseInt(sortParam);
} catch (NumberFormatException nfe) {}
}
Collections.sort(fileList, Sorters.getFileComparator(sort, this));
}
// second table - dir info
buf.append("<table class=\"snarkDirInfo\"><thead>\n");
buf.append("<tr>\n")
.append("<th colspan=2>");
String tx = _t("Directory");
// cycle through sort by name or type
String sort;
boolean isTypeSort = false;
if (showSort) {
if (sortParam == null || "0".equals(sortParam) || "1".equals(sortParam)) {
sort = "-1";
} else if ("-1".equals(sortParam)) {
sort = "12";
isTypeSort = true;
} else if ("12".equals(sortParam)) {
sort = "-12";
isTypeSort = true;
} else {
sort = "";
}
buf.append("<a href=\"").append(base)
.append(getQueryString(sort)).append("\">");
}
toThemeImg(buf, "file", tx,
showSort ? _t("Sort by {0}", (isTypeSort ? _t("File type") : _t("Name")))
: tx + ": " + directory);
if (showSort)
buf.append("</a>");
int dirSlash = directory.indexOf('/');
if (dirSlash > 0) {
buf.append(" ");
buf.append(DataHelper.escapeHTML(directory.substring(dirSlash + 1)));
}
buf.append("</th>\n<th align=\"right\">");
if (showSort) {
sort = ("5".equals(sortParam)) ? "-5" : "5";
buf.append("<a href=\"").append(base)
.append(getQueryString(sort)).append("\">");
}
tx = _t("Size");
toThemeImg(buf, "size", tx,
showSort ? _t("Sort by {0}", tx) : tx);
if (showSort)
buf.append("</a>");
buf.append("</th>\n<th class=\"headerstatus\">");
boolean showRemainingSort = showSort && showPriority;
if (showRemainingSort) {
sort = ("10".equals(sortParam)) ? "-10" : "10";
buf.append("<a href=\"").append(base)
.append(getQueryString(sort)).append("\">");
}
tx = _t("Status");
toThemeImg(buf, "status", tx,
showRemainingSort ? _t("Sort by {0}", _t("Remaining")) : tx);
if (showRemainingSort)
buf.append("</a>");
if (showPriority) {
buf.append("</th>\n<th class=\"headerpriority\">");
if (showSort) {
sort = ("13".equals(sortParam)) ? "-13" : "13";
buf.append("<a href=\"").append(base)
.append(getQueryString(sort)).append("\">");
}
tx = _t("Priority");
toThemeImg(buf, "priority", tx,
showSort ? _t("Sort by {0}", tx) : tx);
if (showSort)
buf.append("</a>");
}
buf.append("</th>\n</tr>\n</thead>\n");
buf.append("<tr><td colspan=\"" + (showPriority ? '5' : '4') + "\" class=\"ParentDir\"><A HREF=\"");
URIUtil.encodePath(buf, addPaths(decodedBase,"../"));
buf.append("\">");
toThemeImg(buf, "up");
buf.append(' ')
.append(_t("Up to higher level directory"))
.append("</A></td></tr>\n");
//DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
// DateFormat.MEDIUM);
boolean showSaveButton = false;
boolean rowEven = true;
for (Sorters.FileAndIndex fai : fileList)
{
//String encoded = encodePath(ls[i].getName());
// bugfix for I2P - Backport from Jetty 6 (zero file lengths and last-modified times)
// http://jira.codehaus.org/browse/JETTY-361?page=com.atlassian.jira.plugin.system.issuetabpanels%3Achangehistory-tabpanel#issue-tabs
// See resource.diff attachment
//Resource item = addPath(encoded);
File item = fai.file;
String rowClass = (rowEven ? "snarkTorrentEven" : "snarkTorrentOdd");
rowEven = !rowEven;
buf.append("<TR class=\"").append(rowClass).append("\">");
// Get completeness and status string
boolean complete = false;
String status = "";
long length = item.length();
int fileIndex = fai.index;
int priority = 0;
if (fai.isDirectory) {
complete = true;
//status = toImg("tick") + ' ' + _t("Directory");
} else {
if (snark == null || snark.getStorage() == null) {
// Assume complete, perhaps he removed a completed torrent but kept a bookmark
complete = true;
status = toImg("cancel") + ' ' + _t("Torrent not found?");
} else {
long remaining = fai.remaining;
if (remaining < 0) {
complete = true;
status = toImg("cancel") + ' ' + _t("File not found in torrent?");
} else if (remaining == 0 || length <= 0) {
complete = true;
status = toImg("tick") + ' ' + _t("Complete");
} else {
priority = fai.priority;
if (priority < 0)
status = toImg("cancel");
else if (priority == 0)
status = toImg("clock");
else
status = toImg("clock_red");
status += " " +
(100 * (length - remaining) / length) + "% " + _t("complete") +
" (" + DataHelper.formatSize2(remaining) + "B " + _t("remaining") + ")";
}
}
}
String path = addPaths(decodedBase, item.getName());
if (item.isDirectory() && !path.endsWith("/"))
path=addPaths(path,"/");
path = encodePath(path);
String icon = toIcon(item);
buf.append("<TD class=\"snarkFileIcon\">");
if (complete) {
buf.append("<a href=\"").append(path).append("\">");
// thumbnail ?
String plc = item.toString().toLowerCase(Locale.US);
if (plc.endsWith(".jpg") || plc.endsWith(".jpeg") || plc.endsWith(".png") ||
plc.endsWith(".gif") || plc.endsWith(".ico")) {
buf.append("<img alt=\"\" border=\"0\" class=\"thumb\" src=\"")
.append(path).append("\"></a>");
} else {
buf.append(toImg(icon, _t("Open"))).append("</a>");
}
} else {
buf.append(toImg(icon));
}
buf.append("</TD><TD class=\"snarkFileName\">");
if (complete)
buf.append("<a href=\"").append(path).append("\">");
buf.append(DataHelper.escapeHTML(item.getName()));
if (complete)
buf.append("</a>");
buf.append("</TD><TD ALIGN=right class=\"snarkFileSize\">");
if (!item.isDirectory())
buf.append(DataHelper.formatSize2(length)).append('B');
buf.append("</TD><TD class=\"snarkFileStatus\">");
//buf.append(dfmt.format(new Date(item.lastModified())));
buf.append(status);
buf.append("</TD>");
if (showPriority) {
buf.append("<td class=\"priority\">");
if ((!complete) && (!item.isDirectory())) {
buf.append("\n<input type=\"radio\" onclick=\"priorityclicked();\" class=\"prihigh\" value=\"5\" name=\"pri.").append(fileIndex).append("\" ");
if (priority > 0)
buf.append("checked=\"checked\"");
buf.append('>').append(_t("High"));
buf.append("\n<input type=\"radio\" onclick=\"priorityclicked();\" class=\"prinorm\" value=\"0\" name=\"pri.").append(fileIndex).append("\" ");
if (priority == 0)
buf.append("checked=\"checked\"");
buf.append('>').append(_t("Normal"));
buf.append("\n<input type=\"radio\" onclick=\"priorityclicked();\" class=\"priskip\" value=\"-9\" name=\"pri.").append(fileIndex).append("\" ");
if (priority < 0)
buf.append("checked=\"checked\"");
buf.append('>').append(_t("Skip"));
showSaveButton = true;
}
buf.append("</td>");
}
buf.append("</TR>\n");
}
if (showSaveButton) {
buf.append("<thead><tr><th colspan=\"4\"> </th><th class=\"headerpriority\">" +
"<a class=\"control\" id=\"setallhigh\" href=\"javascript:void(null);\" onclick=\"setallhigh();\">")
.append(toImg("clock_red")).append(_t("Set all high")).append("</a>\n" +
"<a class=\"control\" id=\"setallnorm\" href=\"javascript:void(null);\" onclick=\"setallnorm();\">")
.append(toImg("clock")).append(_t("Set all normal")).append("</a>\n" +
"<a class=\"control\" id=\"setallskip\" href=\"javascript:void(null);\" onclick=\"setallskip();\">")
.append(toImg("cancel")).append(_t("Skip all")).append("</a>\n" +
"<br><br><input type=\"submit\" class=\"accept\" value=\"").append(_t("Save priorities"))
.append("\" name=\"savepri\" >\n" +
"</th></tr></thead>\n");
}
buf.append("</table>\n");
// for stop/start/check
if (showStopStart || showPriority)
buf.append("</form>");
buf.append("</div></div></BODY></HTML>\n");
return buf.toString();
}
/**
* @param so null ok
* @return query string or ""
* @since 0.9.16
*/
private static String getQueryString(String so) {
if (so != null && !so.equals(""))
return "?sort=" + DataHelper.stripHTML(so);
return "";
}
/**
* Pick an icon; try to catch the common types in an i2p environment.
*
* @return file name not including ".png"
* @since 0.7.14
*/
private String toIcon(File item) {
if (item.isDirectory())
return "folder";
return toIcon(item.toString());
}
/**
* Pick an icon; try to catch the common types in an i2p environment
* Pkg private for FileTypeSorter.
*
* @return file name not including ".png"
* @since 0.7.14
*/
String toIcon(String path) {
String icon;
// Note that for this to work well, our custom mime.properties file must be loaded.
String plc = path.toLowerCase(Locale.US);
String mime = getMimeType(path);
if (mime == null)
mime = "";
if (mime.equals("text/html"))
icon = "html";
else if (mime.equals("text/plain") ||
mime.equals("text/x-sfv") ||
mime.equals("application/rtf") ||
mime.equals("application/epub+zip") ||
mime.equals("application/x-mobipocket-ebook") ||
plc.endsWith(".azw4"))
icon = "page";
else if (mime.equals("application/java-archive") ||
plc.endsWith(".deb"))
icon = "package";
else if (plc.endsWith(".xpi2p"))
icon = "plugin";
else if (mime.equals("application/pdf"))
icon = "page_white_acrobat";
else if (mime.startsWith("image/"))
icon = "photo";
else if (mime.startsWith("audio/") || mime.equals("application/ogg"))
icon = "music";
else if (mime.startsWith("video/"))
icon = "film";
else if (mime.equals("application/zip")) {
if (plc.endsWith(".su3") || plc.endsWith(".su2") || plc.endsWith(".sud"))
icon = "itoopie_xxsm";
else
icon = "compress";
} else if (mime.equals("application/x-gtar") || mime.equals("application/x-xz") ||
mime.equals("application/compress") || mime.equals("application/gzip") ||
mime.equals("application/x-7z-compressed") || mime.equals("application/x-rar-compressed") ||
mime.equals("application/x-tar") || mime.equals("application/x-bzip2"))
icon = "compress";
else if (plc.endsWith(".exe"))
icon = "application";
else if (plc.endsWith(".iso"))
icon = "cd";
else if (mime.equals("application/x-bittorrent"))
icon = "magnet";
else
icon = "page_white";
return icon;
}
/**
* Icon file in the .war. Always 16x16.
*
* @param icon name without the ".png"
* @since 0.7.14
*/
private String toImg(String icon) {
return toImg(icon, "");
}
/**
* Icon file in the .war. Always 16x16.
*
* @param icon name without the ".png"
* @since 0.8.2
*/
private String toImg(String icon, String altText) {
return "<img alt=\"" + altText + "\" height=\"16\" width=\"16\" src=\"" + _contextPath + WARBASE + "icons/" + icon + ".png\">";
}
/**
* Image file in the theme.
*
* @param image name without the ".png"
* @since 0.9.16
*/
private String toThemeImg(String image) {
return toThemeImg(image, "", "");
}
/**
* Image file in the theme.
*
* @param image name without the ".png"
* @since 0.9.16
*/
private void toThemeImg(StringBuilder buf, String image) {
toThemeImg(buf, image, "", "");
}
/**
* Image file in the theme.
*
* @param image name without the ".png"
* @param altText non-null
* @param titleText non-null
* @since 0.9.16
*/
private String toThemeImg(String image, String altText, String titleText) {
StringBuilder buf = new StringBuilder(128);
toThemeImg(buf, image, altText, titleText);
return buf.toString();
}
/**
* Image file in the theme.
*
* @param image name without the ".png"
* @param altText non-null
* @param titleText non-null
* @since 0.9.16
*/
private void toThemeImg(StringBuilder buf, String image, String altText, String titleText) {
buf.append("<img alt=\"").append(altText).append("\" src=\"").append(_imgPath).append(image).append(".png\"");
if (titleText.length() > 0)
buf.append(" title=\"").append(titleText).append('"');
buf.append('>');
}
/** @since 0.8.1 */
private void savePriorities(Snark snark, Map<String, String[]> postParams) {
Storage storage = snark.getStorage();
if (storage == null)
return;
for (Map.Entry<String, String[]> entry : postParams.entrySet()) {
String key = entry.getKey();
if (key.startsWith("pri.")) {
try {
int fileIndex = Integer.parseInt(key.substring(4));
String val = entry.getValue()[0]; // jetty arrays
int pri = Integer.parseInt(val);
storage.setPriority(fileIndex, pri);
//System.err.println("Priority now " + pri + " for " + file);
} catch (Throwable t) { t.printStackTrace(); }
}
}
snark.updatePiecePriorities();
_manager.saveTorrentStatus(snark);
}
/**
* Is "a" equal to "b",
* or is "a" a directory and a parent of file or directory "b",
* canonically speaking?
*
* @since 0.9.15
*/
private static boolean isParentOf(File a, File b) {
try {
a = a.getCanonicalFile();
b = b.getCanonicalFile();
} catch (IOException ioe) {
return false;
}
if (a.equals(b))
return true;
if (!a.isDirectory())
return false;
// easy case
if (!b.getPath().startsWith(a.getPath()))
return false;
// dir by dir
while (!a.equals(b)) {
b = b.getParentFile();
if (b == null)
return false;
}
return true;
}
}