forked from I2P_Developers/i2p.i2p
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
95bcf2977a | |||
4f5a236fdc | |||
688638ad69 |
@ -89,6 +89,7 @@ public class I2PSnarkUtil implements DisconnectListener {
|
||||
private long _startedTime;
|
||||
private final DisconnectListener _discon;
|
||||
private int _maxFilesPerTorrent = SnarkManager.DEFAULT_MAX_FILES_PER_TORRENT;
|
||||
private String _apiTarget, _apiKey;
|
||||
|
||||
private static final int EEPGET_CONNECT_TIMEOUT = 45*1000;
|
||||
private static final int EEPGET_CONNECT_TIMEOUT_SHORT = 5*1000;
|
||||
@ -259,6 +260,21 @@ public class I2PSnarkUtil implements DisconnectListener {
|
||||
/** @since 0.9.58 */
|
||||
public void setMaxFilesPerTorrent(int max) { _maxFilesPerTorrent = Math.max(max, 1); }
|
||||
|
||||
/** @since 0.9.67 */
|
||||
public String getAPITarget() { return _apiTarget; };
|
||||
/** @since 0.9.67 */
|
||||
public String getAPIKey() { return _apiKey; };
|
||||
/** @since 0.9.67 */
|
||||
public void setAPI(String target, String key) {
|
||||
_apiTarget = target;
|
||||
_apiKey = key;
|
||||
}
|
||||
/** @since 0.9.67 */
|
||||
public boolean hasAPIKey() {
|
||||
return _apiTarget != null && _apiTarget.length() > 0 &&
|
||||
_apiKey != null && _apiKey.length() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the router, if we aren't already
|
||||
*/
|
||||
|
@ -170,6 +170,8 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
|
||||
private static final String PROP_COMMENTS_NAME = "i2psnark.commentsName";
|
||||
/** @since 0.9.58 */
|
||||
public static final String PROP_MAX_FILES_PER_TORRENT = "i2psnark.maxFilesPerTorrent";
|
||||
/** @since 0.9.67 */
|
||||
private static final String PROP_API_PREFIX = "i2psnark.apikey.";
|
||||
|
||||
public static final int MIN_UP_BW = 5;
|
||||
public static final int MIN_DOWN_BW = 2 * MIN_UP_BW;
|
||||
@ -1055,6 +1057,15 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
|
||||
_util.setCommentsName(_config.getProperty(PROP_COMMENTS_NAME, ""));
|
||||
_util.setCollapsePanels(Boolean.parseBoolean(_config.getProperty(PROP_COLLAPSE_PANELS,
|
||||
Boolean.toString(I2PSnarkUtil.DEFAULT_COLLAPSE_PANELS))));
|
||||
for (String c : _config.stringPropertyNames()) {
|
||||
if (c.startsWith(PROP_API_PREFIX)) {
|
||||
String tgt = c.substring(PROP_API_PREFIX.length());
|
||||
String key = _config.getProperty(c);
|
||||
// we only support one for now
|
||||
_util.setAPI(tgt, key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
File dd = getDataDir();
|
||||
if (dd.isDirectory()) {
|
||||
if (!dd.canWrite())
|
||||
@ -1084,13 +1095,15 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
|
||||
String startDelay, String pageSize, String seedPct, String eepHost,
|
||||
String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts,
|
||||
String upLimit, String upBW, String downBW, boolean useOpenTrackers, boolean useDHT, String theme,
|
||||
String lang, boolean enableRatings, boolean enableComments, String commentName, boolean collapsePanels) {
|
||||
String lang, boolean enableRatings, boolean enableComments, String commentName, boolean collapsePanels,
|
||||
String apiTarget, String apiKey) {
|
||||
synchronized(_configLock) {
|
||||
locked_updateConfig(dataDir, filesPublic, autoStart, smartSort, refreshDelay,
|
||||
startDelay, pageSize, seedPct, eepHost,
|
||||
eepPort, i2cpHost, i2cpPort, i2cpOpts,
|
||||
upLimit, upBW, downBW, useOpenTrackers, useDHT, theme,
|
||||
lang, enableRatings, enableComments, commentName, collapsePanels);
|
||||
lang, enableRatings, enableComments, commentName, collapsePanels,
|
||||
apiTarget, apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1098,7 +1111,8 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
|
||||
String startDelay, String pageSize, String seedPct, String eepHost,
|
||||
String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts,
|
||||
String upLimit, String upBW, String downBW, boolean useOpenTrackers, boolean useDHT, String theme,
|
||||
String lang, boolean enableRatings, boolean enableComments, String commentName, boolean collapsePanels) {
|
||||
String lang, boolean enableRatings, boolean enableComments, String commentName, boolean collapsePanels,
|
||||
String apiTarget, String apiKey) {
|
||||
boolean changed = false;
|
||||
boolean interruptMonitor = false;
|
||||
//if (eepHost != null) {
|
||||
@ -1449,6 +1463,21 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList
|
||||
_util.setCollapsePanels(collapsePanels);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (apiKey != null && apiKey.length() > 0 &&
|
||||
apiTarget != null && apiTarget.length() > 0) {
|
||||
apiKey = DataHelper.stripHTML(apiKey.trim());
|
||||
apiTarget = DataHelper.stripHTML(apiTarget.trim());
|
||||
String oldk = _util.getAPIKey();
|
||||
String oldt = _util.getAPITarget();
|
||||
if (!apiKey.equals(oldk) || !apiTarget.equals(oldt)) {
|
||||
_config.setProperty(PROP_API_PREFIX + apiTarget, apiKey);
|
||||
_util.setAPI(apiTarget, apiKey);
|
||||
addMessage(_t("API key updated."));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
saveConfig();
|
||||
if (interruptMonitor)
|
||||
|
@ -85,8 +85,8 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
|
||||
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 static final String WARBASE = "/.resources/";
|
||||
static final char HELLIP = '\u2026';
|
||||
|
||||
public I2PSnarkServlet() {
|
||||
super();
|
||||
@ -227,6 +227,10 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
PrintWriter out = resp.getWriter();
|
||||
//if (_log.shouldLog(Log.DEBUG))
|
||||
// _manager.addMessage((_context.clock().now() / 1000) + " xhr1 p=" + req.getParameter("p"));
|
||||
String search = req.getParameter("nf_s");
|
||||
boolean isSearch = search != null && search.length() > 0;
|
||||
// hide message box if we're searching
|
||||
if (!isSearch)
|
||||
writeMessages(out, false, peerString);
|
||||
boolean canWrite;
|
||||
synchronized(this) {
|
||||
@ -234,6 +238,27 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
}
|
||||
writeTorrents(out, req, canWrite);
|
||||
return;
|
||||
} else if ("/.ajax/xhr2.html".equals(path)) {
|
||||
// AJAX for remote lookup
|
||||
setHTMLHeaders(resp, cspNonce, false);
|
||||
PrintWriter out = resp.getWriter();
|
||||
String search = req.getParameter("nf_s");
|
||||
boolean isValidRemote = search != null && search.length() >= RemoteSearch.MIN_LEN;
|
||||
if (!isValidRemote) {
|
||||
resp.sendError(404);
|
||||
return;
|
||||
}
|
||||
RemoteSearch rs = new RemoteSearch(_context, _manager);
|
||||
search = decodePath(search);
|
||||
String results = rs.search(search, true);
|
||||
if (results != null) {
|
||||
if (!results.equals(RemoteSearch.OLDER))
|
||||
out.write(results);
|
||||
} else {
|
||||
out.write("<tr class=\"snarkTorrentNoneLoaded\"><td colspan=\"8\">");
|
||||
out.write(_t("No torrents found."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isConfigure = "/configure".equals(path);
|
||||
@ -398,8 +423,20 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
if (!isConfigure) {
|
||||
String search = req.getParameter("nf_s");
|
||||
if (_manager.getTorrents().size() > 1 || (search != null && search.length() > 0)) {
|
||||
out.write("<form class=\"search\" id = \"search\" action=\"" + _contextPath + "\" method=\"GET\">" +
|
||||
"<input type=\"text\" name=\"nf_s\" size=\"20\" class=\"search\" id=\"searchbox\"");
|
||||
out.write("<form class=\"search\" id = \"search\" action=\"" + _contextPath + "\" method=\"GET\">");
|
||||
if (_manager.util().hasAPIKey() && _manager.util().connected()) {
|
||||
String stype = req.getParameter("searchopt");
|
||||
out.write("\n<input type=\"radio\" name=\"searchopt\" class=\"search\" value=\"1\"" +
|
||||
((stype == null || stype.equals("") || stype.equals("1")) ? " checked" : "") + '>' +
|
||||
_t("Local") +
|
||||
"\n<input type=\"radio\" name=\"searchopt\" class=\"search\" value=\"2\"" +
|
||||
("2".equals(stype) ? " checked" : "") + '>' +
|
||||
_t("Postman") +
|
||||
"\n<input type=\"radio\" name=\"searchopt\" class=\"search\" value=\"3\"" +
|
||||
("3".equals(stype) ? " checked" : "") + '>' +
|
||||
_t("Both"));
|
||||
}
|
||||
out.write("\n<input type=\"text\" name=\"nf_s\" size=\"20\" class=\"search\" id=\"searchbox\"");
|
||||
if (search != null)
|
||||
out.write(" value=\"" + DataHelper.escapeHTML(search) + '"');
|
||||
out.write(">" +
|
||||
@ -411,8 +448,70 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
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"));
|
||||
|
||||
// remote search results
|
||||
String search = req.getParameter("nf_s");
|
||||
boolean isSearch = search != null && search.length() > 0;
|
||||
String stype = req.getParameter("searchopt");
|
||||
boolean isValidRemote = isSearch && search.length() >= RemoteSearch.MIN_LEN &&
|
||||
stype != null && (stype.equals("2") || stype.equals("3"));
|
||||
String nojs = req.getParameter("nojs");
|
||||
// if we have js, we need to include this all the time, so the js can
|
||||
// unhide it when a search starts
|
||||
boolean renderRemote = !isConfigure &&
|
||||
_manager.util().hasAPIKey() && _manager.util().connected() &&
|
||||
(nojs == null || isValidRemote);
|
||||
if (renderRemote) {
|
||||
out.write("<div id=\"remoteresults\"><br><br>\n");
|
||||
// out.write("<form ... for add buttons
|
||||
out.write("<table border=\"0\" id=\"remoteresulttable\" class=\"snarkTorrents");
|
||||
if (!isValidRemote)
|
||||
out.write(" disabled");
|
||||
out.write("\" width=\"100%\"><thead>\n");
|
||||
out.write("<tr><th class=\"snarkGraphicStatus\">");
|
||||
String sl = _t("Seed / Leech");
|
||||
out.write(toThemeImg("status", sl, sl));
|
||||
out.write("<th class=\"snarkTorrentStatus\">");
|
||||
out.write("S / L");
|
||||
//out.write("<th class=\"snarkTorrentDetails\">");
|
||||
out.write("<th colspan=\"2\">");
|
||||
out.write(toThemeImg("torrent"));
|
||||
//out.write("<th class=\"snarkTorrentName\">");
|
||||
String tgt = _manager.util().getAPITarget();
|
||||
out.write("Search results from <a href=\"http://" + tgt + "\">" + tgt + "</a>");
|
||||
out.write("<th class=\"snarkTorrentDownloaded\">");
|
||||
out.write(toThemeImg("head_rx"));
|
||||
//out.write(_t("Size"));
|
||||
out.write("<th class=\"snarkTorrentUploaded\">");
|
||||
out.write(toThemeImg("head_tx"));
|
||||
//out.write(_t("Added"));
|
||||
out.write("<th class=\"snarkTorrentUploaded\">");
|
||||
out.write(toThemeImg("head_tx"));
|
||||
//out.write(_t("Uploader"));
|
||||
out.write("<th class=\"snarkTorrentAction\">");
|
||||
out.write(toThemeImg("add"));
|
||||
//out.write(_t("Add"));
|
||||
// this is what XHR2 replaces
|
||||
out.write("</thead><tbody id=\"remoteresulttablebody\">\n");
|
||||
if (isValidRemote) {
|
||||
RemoteSearch rs = new RemoteSearch(_context, _manager);
|
||||
search = decodePath(search);
|
||||
String results = rs.search(search, false);
|
||||
if (results != null) {
|
||||
out.write(results);
|
||||
} else {
|
||||
out.write("<tr class=\"snarkTorrentNoneLoaded\"><td colspan=\"8\">");
|
||||
out.write(_t("No torrents found."));
|
||||
}
|
||||
}
|
||||
// out.write("</form ...
|
||||
out.write("</tbody></table></div>\n");
|
||||
}
|
||||
|
||||
out.write("<div id=\"page\" class=\"page\"><div id=\"mainsection\" class=\"mainsection\">");
|
||||
|
||||
// hide message box if we're searching
|
||||
if (!isSearch)
|
||||
writeMessages(out, isConfigure, peerString);
|
||||
|
||||
if (isConfigure) {
|
||||
@ -425,7 +524,14 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
synchronized(this) {
|
||||
canWrite = _resourceBase.canWrite();
|
||||
}
|
||||
boolean pageOne = writeTorrents(out, req, canWrite);
|
||||
boolean pageOne;
|
||||
if (!isSearch || !"2".equals(stype)) {
|
||||
// hide for remote-only search
|
||||
pageOne = writeTorrents(out, req, canWrite);
|
||||
} else {
|
||||
// don't show forms for remote-only search
|
||||
pageOne = false;
|
||||
}
|
||||
// end of mainsection div
|
||||
if (pageOne) {
|
||||
out.write("</div><div id=\"lowersection\">\n");
|
||||
@ -464,7 +570,7 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
private void writeMessages(PrintWriter out, boolean isConfigure, String peerString) throws IOException {
|
||||
List<UIMessages.Message> msgs = _manager.getMessages();
|
||||
if (!msgs.isEmpty()) {
|
||||
out.write("\n<div class=\"snarkMessages\" tabindex=\"0\">" +
|
||||
out.write("\n<div class=\"snarkMessages\" id=\"snarkMessages\" tabindex=\"0\">" +
|
||||
"<a id=\"closeLog\" href=\"" + _contextPath + '/');
|
||||
if (isConfigure)
|
||||
out.write("configure");
|
||||
@ -500,6 +606,7 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
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");
|
||||
@ -517,12 +624,16 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
boolean isSearch = false;
|
||||
String search = req.getParameter("nf_s");
|
||||
if (search != null && search.length() > 0) {
|
||||
String stype = req.getParameter("searchopt");
|
||||
boolean local = stype == null || stype.equals("1") || stype.equals("3");
|
||||
if (local) {
|
||||
List<Snark> matches = search(search, snarks);
|
||||
if (matches != null) {
|
||||
snarks = matches;
|
||||
isSearch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pages
|
||||
int start = 0;
|
||||
@ -952,6 +1063,7 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
.append(DataHelper.escapeHTML(sParam)).append("\" >\n");
|
||||
}
|
||||
}
|
||||
buf.append("<noscript><input type=\"hidden\" name=\"nojs\" value=\"1\"></noscript>\n");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1478,10 +1590,13 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
// commentsName is filtered in SnarkManager.updateConfig()
|
||||
String commentsName = req.getParameter("nofilter_commentsName");
|
||||
boolean collapsePanels = req.getParameter("collapsePanels") != null;
|
||||
String apiTarget = req.getParameter("apiTarget");
|
||||
String apiKey = req.getParameter("apiKey");
|
||||
_manager.updateConfig(dataDir, filesPublic, autoStart, smartSort, refreshDel, startupDel, pageSize,
|
||||
seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts,
|
||||
upLimit, upBW, downBW, useOpenTrackers, useDHT, theme,
|
||||
lang, ratings, comments, commentsName, collapsePanels);
|
||||
lang, ratings, comments, commentsName, collapsePanels,
|
||||
apiTarget, apiKey);
|
||||
// update servlet
|
||||
try {
|
||||
setResourceBase(_manager.getDataDir());
|
||||
@ -1801,7 +1916,7 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
return rv;
|
||||
}
|
||||
|
||||
private static final int MAX_DISPLAYED_FILENAME_LENGTH = 50;
|
||||
static final int MAX_DISPLAYED_FILENAME_LENGTH = 50;
|
||||
private static final int MAX_DISPLAYED_ERROR_LENGTH = 43;
|
||||
|
||||
/**
|
||||
@ -2957,8 +3072,39 @@ public class I2PSnarkServlet extends BasicServlet {
|
||||
out.write("<tr><td>");
|
||||
out.write(_t("I2CP options"));
|
||||
out.write(":<td colspan=\"2\"><textarea name=\"i2cpOpts\" cols=\"60\" rows=\"1\" wrap=\"off\" spellcheck=\"false\" >"
|
||||
+ opts.toString() + "</textarea>\n" +
|
||||
"<tr class=\"spacer\"><td colspan=\"3\"> \n" + // spacer
|
||||
+ opts.toString() + "</textarea>\n");
|
||||
|
||||
out.write("<tr><td>");
|
||||
out.write(_t("Tracker Search API Key"));
|
||||
String target = _manager.util().getAPITarget();
|
||||
if (target == null)
|
||||
target = "tracker2.postman.i2p";
|
||||
String key = _manager.util().getAPIKey();
|
||||
if (key != null)
|
||||
key = DataHelper.escapeHTML(key);
|
||||
else
|
||||
key = "";
|
||||
out.write(":<td>" +
|
||||
"<input type=\"text\" name=\"apiTarget\" value=\"" + target + "\" readonly=\"readonly\">\n" +
|
||||
"<input type=\"password\" name=\"apiKey\" size=\"24\" value=\"" + key + "\">\n" +
|
||||
"<td id=\"bwHelp\"><i>" +
|
||||
_t("Required for tracker search") +
|
||||
"</i> <a href=\"http://tracker2.postman.i2p/index.php?view=MyAccount\" target=\"blank\">[" +
|
||||
_t("Get API Key") +
|
||||
"]</a>\n");
|
||||
|
||||
out.write("<tr><td>");
|
||||
out.write(_t("Tracker Search All Categories"));
|
||||
boolean allcats = false;
|
||||
out.write(":<td><input type=\"checkbox\" class=\"optbox\" name=\"allcats\" id=\"allcats\" value=\"true\" " +
|
||||
(allcats ? "checked " : "") +
|
||||
"</td><td id=\"bwHelp\"><i>" +
|
||||
_t("If unchecked, use account default") +
|
||||
"</i> <a href=\"http://tracker2.postman.i2p/index.php?view=MyAccount\" target=\"blank\">[" +
|
||||
_t("Settings") +
|
||||
"]</a>\n");
|
||||
|
||||
out.write("<tr class=\"spacer\"><td colspan=\"3\"> \n" + // spacer
|
||||
"<tr><td colspan=\"3\"><input type=\"submit\" class=\"accept\" value=\"");
|
||||
out.write(_t("Save configuration"));
|
||||
out.write("\" name=\"foo\" >\n" +
|
||||
|
640
apps/i2psnark/java/src/org/klomp/snark/web/RemoteSearch.java
Normal file
640
apps/i2psnark/java/src/org/klomp/snark/web/RemoteSearch.java
Normal file
@ -0,0 +1,640 @@
|
||||
package org.klomp.snark.web;
|
||||
|
||||
/*
|
||||
* Released into the public domain
|
||||
* with no warranty of any kind, either expressed or implied.
|
||||
*/
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import net.i2p.CoreVersion;
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.client.streaming.I2PSocketEepGet;
|
||||
import net.i2p.client.streaming.I2PSocketManager;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.servlet.util.ServletUtil;
|
||||
import net.i2p.util.ByteArrayStream;
|
||||
import net.i2p.util.EepGet;
|
||||
import net.i2p.util.LHMCache;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
import org.json.simple.DeserializationException;
|
||||
import org.json.simple.Jsoner;
|
||||
import org.json.simple.JsonArray;
|
||||
import org.json.simple.JsonKey;
|
||||
import org.json.simple.JsonObject;
|
||||
|
||||
import org.klomp.snark.I2PSnarkUtil;
|
||||
import org.klomp.snark.Snark;
|
||||
import org.klomp.snark.SnarkManager;
|
||||
import org.klomp.snark.URIUtil;
|
||||
|
||||
/**
|
||||
* Search a remote site using its API. Only one site supported for now.
|
||||
*
|
||||
* Ref: http://tracker2.postman.i2p/api.md
|
||||
*
|
||||
* @since 0.9.67
|
||||
*/
|
||||
class RemoteSearch {
|
||||
|
||||
// statics for caching, dup checks, ordering checks, ...
|
||||
// TODO this may behave strangely if two search tabs open at once
|
||||
private static final AtomicInteger _lastSent = new AtomicInteger();
|
||||
private static final AtomicInteger _lastRcvd = new AtomicInteger();
|
||||
private static final AtomicReference<String> _currentSearch = new AtomicReference<String>();
|
||||
private static final AtomicLong _bannedUntil = new AtomicLong();
|
||||
// positive/negative cache, Result.result may be null
|
||||
private static final Map<String, Result> _cache = new LHMCache<String, Result>(16);
|
||||
//private static final List<Thread> _waiting = new ArrayList<Thread>(16);
|
||||
private static final Object _waitLock = new Object();
|
||||
private static Thread _waiting;
|
||||
|
||||
private final I2PAppContext _ctx;
|
||||
private final Log _log;
|
||||
private final SnarkManager _manager;
|
||||
private final int _id;
|
||||
private final int _rcvd;
|
||||
|
||||
/** fixme for mulitple installs */
|
||||
private static final String _contextPath = "/i2psnark";
|
||||
private static final String _themePathPfx = _contextPath + I2PSnarkServlet.WARBASE + "themes/";
|
||||
private final String _themePath;
|
||||
private final String _imgPath;
|
||||
|
||||
private static final String SUPPORTED_TARGET = "tracker2.postman.i2p";
|
||||
private static final String API_TARGET = "http://" + SUPPORTED_TARGET + "/api/torrents";
|
||||
private static final String DETAILS_URL = "http://" + SUPPORTED_TARGET + "/index.php?view=TorrentDetail&id=";
|
||||
private static final String API_HEADER = "X-Postman-Token";
|
||||
private static final String WEB_URL = "http://" + SUPPORTED_TARGET + "/?view=Main&lastactive=-1&category=-1&orderby=-1&lang=-1&search=";
|
||||
public static final int MIN_LEN = 3;
|
||||
private static final long CACHE_LIFETIME = 10*60*1000;
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
|
||||
private static final long HEADER_TIMEOUT = 20*1000;
|
||||
private static final long TOTAL_TIMEOUT = 30*1000;
|
||||
private static final long INACTIVITY_TIMEOUT = -1;
|
||||
private static final long DATE_FORMAT_CUTOFF = 3*24*60*60*1000L;
|
||||
private static final long DELAY = 650;
|
||||
// special return value if older
|
||||
public static final String OLDER = "!";
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public RemoteSearch(I2PAppContext ctx, SnarkManager mgr) {
|
||||
_ctx = ctx;
|
||||
_log = ctx.logManager().getLog(RemoteSearch.class);
|
||||
_manager = mgr;
|
||||
_id = _lastSent.incrementAndGet();
|
||||
_rcvd = _lastRcvd.get();
|
||||
_themePath = _themePathPfx + _manager.getTheme() + '/';
|
||||
_imgPath = _themePath + "images/";
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns table rows only, to be inserted into the remote results table.
|
||||
* Blocking.
|
||||
*
|
||||
* @param s URI-decoded
|
||||
* @return null on failure or no results, OLDER if the result should be discarded.
|
||||
*/
|
||||
public String search(String s, boolean isXHR) {
|
||||
// don't kick off a new search after a space was typed,
|
||||
// trim it and pull from cache
|
||||
s = s.trim();
|
||||
if (s.length() < MIN_LEN)
|
||||
return null;
|
||||
I2PSnarkUtil util = _manager.util();
|
||||
boolean connected = util.connected();
|
||||
synchronized(_cache) {
|
||||
Result result = _cache.get(s);
|
||||
if (result != null) {
|
||||
if (!connected)
|
||||
return result.result;
|
||||
long now = _ctx.clock().now();
|
||||
if (result.time + CACHE_LIFETIME >= now) {
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Cached: " + s);
|
||||
return result.result;
|
||||
} else {
|
||||
_cache.remove(s);
|
||||
}
|
||||
}
|
||||
// while we have the lock
|
||||
if ((_id & 0x08) == 0)
|
||||
expireCache();
|
||||
}
|
||||
|
||||
if (!connected)
|
||||
return null;
|
||||
I2PSocketManager smanager = util.getSocketManager();
|
||||
if (smanager == null)
|
||||
return null;
|
||||
String tgt = util.getAPITarget();
|
||||
if (!SUPPORTED_TARGET.equals(tgt))
|
||||
return null;
|
||||
String key = util.getAPIKey();
|
||||
if (key == null)
|
||||
return null;
|
||||
|
||||
// dup check
|
||||
// for example, if user types and then hits enter, we'll get
|
||||
// an XHR and then a non-XHR for the same string
|
||||
String prev = _currentSearch.getAndSet(s);
|
||||
if (s.equals(prev)) {
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Dup search for " + s + " waiting for result");
|
||||
for (int i = 0; i < 60; i++) {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException ie) {
|
||||
return OLDER;
|
||||
}
|
||||
synchronized(_cache) {
|
||||
Result result = _cache.get(s);
|
||||
if (result != null) {
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Got result from dup lookup " + s);
|
||||
return result.result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
long currentBan = _bannedUntil.get();
|
||||
if (currentBan > 0) {
|
||||
long now = _ctx.clock().now();
|
||||
if (currentBan > now) {
|
||||
long retry = currentBan - now;
|
||||
if (retry <= 10*60*1000)
|
||||
return _t("Rate limited for {0}", DataHelper.formatDuration(retry));
|
||||
else
|
||||
return _t("Rate limited until {0}", DataHelper.formatTime(currentBan));
|
||||
}
|
||||
_bannedUntil.compareAndSet(currentBan, 0);
|
||||
}
|
||||
|
||||
// if a new search comes in within 650 ms, interrupt the old one
|
||||
if (isXHR) {
|
||||
Thread us = Thread.currentThread();
|
||||
synchronized(_waitLock) {
|
||||
if (_waiting != null) {
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Search for " + s + " interrupting 1 pending lookup");
|
||||
_waiting.interrupt();
|
||||
}
|
||||
_waiting = us;
|
||||
try {
|
||||
_waitLock.wait(DELAY);
|
||||
} catch (InterruptedException ie) {
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("interrupted, aborting: " + s);
|
||||
return OLDER;
|
||||
}
|
||||
_waiting = null;
|
||||
}
|
||||
}
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("starting fetch: " + s);
|
||||
|
||||
String encoded = URIUtil.encodePath(s);
|
||||
int limit = Math.min(100, Math.max(5, _manager.getPageSize()));
|
||||
// includes description search
|
||||
//String url = API_TARGET + "?search=" + encoded + "&entries=" + limit;
|
||||
// torrent name only
|
||||
//String url = API_TARGET + "?name=" + encoded + "&entries=" + limit;
|
||||
// include inactive and all categories
|
||||
// set defaults at http://tracker2.postman.i2p/?view=MyAccount
|
||||
// TODO config for catfilter
|
||||
String url = API_TARGET + "?name=" + encoded + "&entries=" + limit + "&filter_id=-1&catfilter_id=-1";
|
||||
int max_size = limit * 2048;
|
||||
ByteArrayStream out = new ByteArrayStream(16384);
|
||||
EepGet get = new I2PSocketEepGet(_ctx, smanager, 0, 0, max_size, null, out, url);
|
||||
get.addHeader("User-Agent", I2PSnarkUtil.EEPGET_USER_AGENT);
|
||||
get.addHeader("Accept", "application/json");
|
||||
get.addHeader(API_HEADER, key);
|
||||
boolean ok = get.fetch(HEADER_TIMEOUT, TOTAL_TIMEOUT, INACTIVITY_TIMEOUT);
|
||||
_currentSearch.compareAndSet(s, null);
|
||||
if (ok) {
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Fetch successful [" + url + "]: size=" + out.size());
|
||||
String rv = processResponse(out, s, encoded);
|
||||
long now = _ctx.clock().now();
|
||||
// positive or negative cache
|
||||
synchronized(_cache) {
|
||||
_cache.put(s, new Result(rv, now));
|
||||
}
|
||||
if (isXHR && isOlder()) {
|
||||
if (_log.shouldWarn())
|
||||
_log.warn("got old response");
|
||||
return OLDER;
|
||||
}
|
||||
return rv;
|
||||
} else {
|
||||
StringBuilder buf = new StringBuilder(128);
|
||||
buf.append("<tr class=\"snarkTorrentNoneLoaded\"><td colspan=\"8\">");
|
||||
int code = get.getStatusCode();
|
||||
if (code == 200) // got header but not body
|
||||
code = -1;
|
||||
if (code > 0) {
|
||||
buf.append(code);
|
||||
String text;
|
||||
if (code == 429)
|
||||
text = process429(out);
|
||||
else
|
||||
text = get.getStatusText();
|
||||
if (text != null)
|
||||
buf.append(' ').append(DataHelper.escapeHTML(text));
|
||||
} else {
|
||||
buf.append(_t("Connection to {0} failed", tgt));
|
||||
}
|
||||
if (_log.shouldInfo())
|
||||
_log.info("Fetch failed [" + url + "] " + buf);
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure an old response doesn't trump a newer one
|
||||
* when received out-of-order
|
||||
*/
|
||||
private boolean isOlder() {
|
||||
int rcvd = _rcvd;
|
||||
while(!_lastRcvd.compareAndSet(rcvd, _id)) {
|
||||
rcvd = _lastRcvd.get();
|
||||
if (rcvd >= _id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String processResponse(ByteArrayStream out, String search, String encoded) {
|
||||
try {
|
||||
return x_processResponse(out, search, encoded);
|
||||
} catch (Exception e) {
|
||||
if (_log.shouldWarn())
|
||||
_log.warn("json error input=\"" + DataHelper.getUTF8(out.toByteArray()), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String x_processResponse(ByteArrayStream out, String search, String encoded) throws Exception {
|
||||
if (out.size() == 0)
|
||||
return null;
|
||||
Reader in = new InputStreamReader(out.asInputStream(), "UTF-8");
|
||||
JsonObject map = (JsonObject) Jsoner.deserialize(in);
|
||||
JsonArray torrents = map.getCollection(TORRENTS);
|
||||
if (torrents == null || torrents.isEmpty()) {
|
||||
if (_log.shouldWarn())
|
||||
_log.warn("no results");
|
||||
return null;
|
||||
}
|
||||
StringBuilder buf = new StringBuilder(torrents.size() * 256);
|
||||
long cutoff = _ctx.clock().now() - DATE_FORMAT_CUTOFF;
|
||||
for (Object o : torrents) {
|
||||
JsonObject torrent = (JsonObject) o;
|
||||
render(buf, torrent, cutoff);
|
||||
}
|
||||
int found = map.getIntegerOrDefault(FOUND);
|
||||
int sz = torrents.size();
|
||||
int more = found - sz;
|
||||
buf.append("\n<tr id=\"remoteresultfooter\" search=\"").append(DataHelper.escapeHTML(search))
|
||||
.append("\"><td colspan=\"2\"> <td colspan=\"2\">");
|
||||
if (more > 0)
|
||||
buf.append(ngettext("{0} more result", "{0} more results", more)).append(" - ");
|
||||
buf.append("<a href=\"").append(WEB_URL).append(encoded)
|
||||
.append("\" target=\"blank\">").append(_t("View results on {0}", SUPPORTED_TARGET)).append("</a><td colspan=\"4\">\n");
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private String process429(ByteArrayStream out) {
|
||||
try {
|
||||
Reader in = new InputStreamReader(out.asInputStream(), "UTF-8");
|
||||
JsonObject map = (JsonObject) Jsoner.deserialize(in);
|
||||
long retry = map.getIntegerOrDefault(RETRY) * 1000L;
|
||||
long until = _ctx.clock().now() + retry;
|
||||
long currentBan = _bannedUntil.get();
|
||||
while (until > currentBan && !_bannedUntil.compareAndSet(currentBan, until)) {
|
||||
currentBan = _bannedUntil.get();
|
||||
}
|
||||
if (retry <= 10*60*1000)
|
||||
return _t("Rate limited for {0}", DataHelper.formatDuration(retry));
|
||||
else
|
||||
return _t("Rate limited until {0}", DataHelper.formatTime(until));
|
||||
} catch (Exception e) {
|
||||
if (_log.shouldWarn())
|
||||
_log.warn("json error input=\"" + DataHelper.getUTF8(out.toByteArray()), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// keys with defaults
|
||||
private static final JsonKey ADDED = Jsoner.mintJsonKey("added", "");
|
||||
private static final JsonKey ALIVE = Jsoner.mintJsonKey("is_alive", Integer.valueOf(0));
|
||||
private static final JsonKey CATEGORY = Jsoner.mintJsonKey("category_id", Integer.valueOf(0));
|
||||
private static final JsonKey FILES = Jsoner.mintJsonKey("file_count", Integer.valueOf(1));
|
||||
private static final JsonKey FOUND = Jsoner.mintJsonKey("available_entries", Integer.valueOf(0));
|
||||
private static final JsonKey HASH = Jsoner.mintJsonKey("info_hash", "");
|
||||
private static final JsonKey ID = Jsoner.mintJsonKey("torrent_id", "0");
|
||||
private static final JsonKey LANG1 = Jsoner.mintJsonKey("languages", null);
|
||||
private static final JsonKey LANG2 = Jsoner.mintJsonKey("main_language_ids", null);
|
||||
private static final JsonKey LANG3 = Jsoner.mintJsonKey("secondary_language_ids", null);
|
||||
private static final JsonKey LEECHES = Jsoner.mintJsonKey("leechers", Integer.valueOf(0));
|
||||
private static final JsonKey LINK = Jsoner.mintJsonKey("download_link", "");
|
||||
private static final JsonKey NAME = Jsoner.mintJsonKey("name", "");
|
||||
private static final JsonKey OWNER = Jsoner.mintJsonKey("owner", "");
|
||||
private static final JsonKey RETRY = Jsoner.mintJsonKey("retry_after", Integer.valueOf(60));
|
||||
private static final JsonKey SEEDS = Jsoner.mintJsonKey("seeders", Integer.valueOf(0));
|
||||
private static final JsonKey SIZE = Jsoner.mintJsonKey("size", Long.valueOf(0));
|
||||
private static final JsonKey TORRENTS = Jsoner.mintJsonKey("torrents", null);
|
||||
|
||||
private void render(StringBuilder buf, JsonObject torrent, long cutoff) {
|
||||
String added = torrent.getStringOrDefault(ADDED);
|
||||
boolean alive = torrent.getIntegerOrDefault(ALIVE) != 0;
|
||||
int cat = torrent.getIntegerOrDefault(CATEGORY);
|
||||
int files = torrent.getIntegerOrDefault(FILES);
|
||||
String hash = torrent.getStringOrDefault(HASH);
|
||||
String id = torrent.getStringOrDefault(ID);
|
||||
int leeches = torrent.getIntegerOrDefault(LEECHES);
|
||||
String link = torrent.getStringOrDefault(LINK);
|
||||
String name = torrent.getStringOrDefault(NAME);
|
||||
String owner = torrent.getStringOrDefault(OWNER);
|
||||
if (owner.equals("hidden"))
|
||||
owner = "";
|
||||
else if (owner.length() > 16)
|
||||
owner = owner.substring(14) + "…";
|
||||
int seeds = torrent.getIntegerOrDefault(SEEDS);
|
||||
long size = torrent.getLongOrDefault(SIZE);
|
||||
|
||||
buf.append("\n<tr");
|
||||
if (seeds <= 0)
|
||||
buf.append(" class=\"dead\"");
|
||||
buf.append("><td class=\"snarkGraphicStatus\">");
|
||||
String img = seeds > 0 ? "seeding"
|
||||
: (leeches > 0 ? "stalled" : "stopped");
|
||||
toThemeImg(buf, img, "", "");
|
||||
buf.append("<td class=\"snarkTorrentStatus\">");
|
||||
buf.append(seeds).append(" / ").append(leeches)
|
||||
.append("<td class=\"snarkTorrentDetails\">");
|
||||
toImg(buf, cat);
|
||||
buf.append("<td class=\"snarkTorrentName\"><a href=\"").append(DETAILS_URL).append(id)
|
||||
.append("\" target=\"_blank\">").append(DataHelper.escapeHTML(truncate(name))).append("</a>")
|
||||
.append("<td class=\"snarkTorrentDownloaded\">").append(DataHelper.formatSize2(size)).append('B')
|
||||
.append(" / ").append(ngettext("{0} file", "{0} files", files))
|
||||
.append("<td class=\"snarkTorrentUploaded\">");
|
||||
long date = parseDate(added);
|
||||
if (date > 0) {
|
||||
if (date < cutoff)
|
||||
buf.append(DataHelper.formatDate(date));
|
||||
else
|
||||
buf.append(DataHelper.formatTime(date));
|
||||
}
|
||||
buf.append("<td class=\"snarkTorrentUploaded\">").append(DataHelper.escapeHTML(owner));
|
||||
buf.append("<td class=\"snarkTorrentAction\">");
|
||||
byte[] ihash = getHash(hash);
|
||||
if (ihash != null) {
|
||||
Snark snark = _manager.getTorrentByInfoHash(ihash);
|
||||
if (snark != null) {
|
||||
String encodedBaseName = URIUtil.encodePath(snark.getBaseName());
|
||||
buf.append("<a href=\"").append(encodedBaseName)
|
||||
.append("/\" title=\"").append(_t("Torrent details"))
|
||||
.append("\">")
|
||||
.append(_t("Already added")).append("</a>");
|
||||
} else {
|
||||
// todo make button link
|
||||
// buf.append(link);
|
||||
String txt = _t("Add torrent");
|
||||
toThemeImg(buf, "add", txt, txt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Result {
|
||||
public final String result;
|
||||
public final long time;
|
||||
|
||||
public Result(String r, long n) {
|
||||
result = r;
|
||||
time = n;
|
||||
}
|
||||
}
|
||||
|
||||
private void expireCache() {
|
||||
cleanCache(_ctx.clock().now());
|
||||
}
|
||||
|
||||
private void cleanCache(long now) {
|
||||
long cutoff = now - CACHE_LIFETIME;
|
||||
synchronized(_cache) {
|
||||
for (Iterator<Result> iter = _cache.values().iterator(); iter.hasNext(); ) {
|
||||
Result r = iter.next();
|
||||
if (r.time < cutoff)
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] getHash(String hash) {
|
||||
if (hash.length() != 40)
|
||||
return null;
|
||||
// Like DataHelper.fromHexString() but ensures no loss of leading zero bytes
|
||||
byte[] rv = new byte[20];
|
||||
try {
|
||||
for (int i = 0; i < 20; i++) {
|
||||
rv[i] = (byte) (Integer.parseInt(hash.substring(i*2, (i*2) + 2), 16) & 0xff);
|
||||
}
|
||||
} catch (NumberFormatException nfe) {
|
||||
rv = null;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
private static long parseDate(String time) {
|
||||
try {
|
||||
Date date = DATE_FORMAT.parse(time);
|
||||
if (date != null)
|
||||
return date.getTime();
|
||||
} catch (ParseException pe) {}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static String truncate(String name) {
|
||||
String rv = name;
|
||||
if (name.length() > I2PSnarkServlet.MAX_DISPLAYED_FILENAME_LENGTH) {
|
||||
String start = ServletUtil.truncate(name, I2PSnarkServlet.MAX_DISPLAYED_FILENAME_LENGTH);
|
||||
if (start.indexOf(' ') < 0 && start.indexOf('-') < 0) {
|
||||
// browser has nowhere to break it
|
||||
rv = start + I2PSnarkServlet.HELLIP;
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
From home page source
|
||||
|
||||
<option value="1">Movies
|
||||
<option value="2">Music
|
||||
<option value="3">TV
|
||||
<option value="4">Games
|
||||
<option value="5">Apps
|
||||
<option value="6">Misc.
|
||||
<option value="8">Pictures
|
||||
<option value="9">Anime
|
||||
<option value="10">Comics
|
||||
<option value="11">Books
|
||||
<option value="13">Music Vid.
|
||||
<option value="14">Pr0n (not in default)
|
||||
<option value="15">Documentary
|
||||
<option value="16">Leaked Documents
|
||||
<option value="17">Audio Books
|
||||
<option value="18">Conspiracy (not in default)
|
||||
<option value="19">Religious Content (not in default)
|
||||
<option value="20">E-Books
|
||||
<option value="21">Course/Lesson
|
||||
<option value="22">Essay/Op-Ed
|
||||
<option value="23">Cad/3D Printing
|
||||
<option value="24">Podcasts
|
||||
*/
|
||||
|
||||
private static final String[] catToText = {
|
||||
"", _x("Movie"), _x("Music"), _x("TV"), _x("Game"), _x("Application"), _x("Other"), "",
|
||||
_x("Pictures"), _x("Anime"), _x("Comics"), _x("Book"), "", _x("Music Video"), _x("Porn"), _x("Documentary"),
|
||||
_x("Leaked Documents"), _x("Audio Book"), _x("Conspiracy"), _x("Religious Content"), _x("E-Book"), _x("Education"), _x("Opinion"), _x("3D Printing"),
|
||||
_x("Podcast")
|
||||
};
|
||||
|
||||
private static final String[] catToIcon = {
|
||||
"", "film", "music", "film", "application", "application", "page_white", "page_white",
|
||||
"photo", "film", "ebook", "ebook", "page_white", "film", "cancel", "film",
|
||||
"page_white_acrobat", "music", "page_white_acrobat", "page_white", "ebook", "page_white_acrobat", "page", "application",
|
||||
"music"
|
||||
};
|
||||
|
||||
private static String toText(int cat) {
|
||||
if (cat <= 0 || cat > catToText.length - 1)
|
||||
return "";
|
||||
return catToText[cat];
|
||||
}
|
||||
|
||||
private static String toIcon(int cat) {
|
||||
String icon;
|
||||
if (cat <= 0 || cat > catToIcon.length - 1)
|
||||
icon = "page_white";
|
||||
else
|
||||
icon = catToIcon[cat];
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon file in the .war. Always 16x16.
|
||||
*
|
||||
* @param icon name without the ".png"
|
||||
*/
|
||||
private void toImg(StringBuilder buf, String icon, String altText) {
|
||||
buf.append("<img alt=\"").append(altText).append("\" title=\"").append(altText)
|
||||
.append("\" height=\"16\" width=\"16\" src=\"").append(_contextPath)
|
||||
.append(I2PSnarkServlet.WARBASE).append("icons/").append(_manager.getThemeIconSet())
|
||||
.append(icon).append(".png?").append(CoreVersion.VERSION).append("\">");
|
||||
}
|
||||
|
||||
private void toImg(StringBuilder buf, int cat) {
|
||||
toImg(buf, toIcon(cat), _t(toText(cat)));
|
||||
}
|
||||
|
||||
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('>');
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
From home page source
|
||||
|
||||
value="1">english
|
||||
value="2">german
|
||||
value="3">french
|
||||
value="4">spanish
|
||||
value="5">portugese
|
||||
value="6">dutch
|
||||
value="7">russian
|
||||
value="8">swedish
|
||||
value="9">italian
|
||||
value="10">chinese
|
||||
value="11">finnish
|
||||
value="12">japanese
|
||||
value="13">turkish
|
||||
value="14">korean
|
||||
value="15">danish
|
||||
value="16">norwegian
|
||||
value="17">polish
|
||||
value="18">hindi
|
||||
*/
|
||||
|
||||
private static final String[] idToLang = {
|
||||
"", "en", "de", "fr", "es", "pt", "nl", "ru",
|
||||
"se", "it", "zh", "fi", "ja", "tr", "ko", "da",
|
||||
"no", "pl", "hi"
|
||||
};
|
||||
|
||||
private static String toLang(int id) {
|
||||
if (id <= 0 || id > idToLang.length - 1)
|
||||
return "en";
|
||||
return idToLang[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param lang two char lower case
|
||||
*/
|
||||
private static int fromLang(String lang) {
|
||||
for (int i = 1; i < idToLang.length; i++) {
|
||||
if (idToLang[i].equals(lang))
|
||||
return i;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** tag */
|
||||
private static String _x(String s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 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) */
|
||||
private String ngettext(String s, String p, int n) {
|
||||
return _manager.util().getString(n, s, p);
|
||||
}
|
||||
}
|
@ -54,6 +54,45 @@ function requestAjax2(refreshtime) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Local fetch, backend does remote query
|
||||
*
|
||||
* @since 0.9.67
|
||||
*/
|
||||
function requestAjaxRemote() {
|
||||
var url = ".ajax/xhr2.html";
|
||||
var query = window.location.search;
|
||||
var box = document.getElementById("searchbox");
|
||||
if (box != null) {
|
||||
// put in, remove, or replace value from search form
|
||||
var search = box.value;
|
||||
if (search.length > 0) {
|
||||
if (query == null) {
|
||||
query = "";
|
||||
}
|
||||
var q = new URLSearchParams(query);
|
||||
q.set("nf_s", encodeURIComponent(search));
|
||||
query = "?" + q.toString();
|
||||
} else {
|
||||
if (query != null) {
|
||||
var q = new URLSearchParams(query);
|
||||
q.delete("nf_s");
|
||||
var newq = q.toString();
|
||||
if (newq != null && newq.length > 0) {
|
||||
query = "?" + newq;
|
||||
} else {
|
||||
query = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (query)
|
||||
url += query;
|
||||
// FIXME this will insert the 'router is down' message on failure
|
||||
ajax(url, "remoteresulttablebody", -1);
|
||||
}
|
||||
|
||||
|
||||
function initAjax() {
|
||||
if (ajaxDelay > 0) {
|
||||
setTimeout(requestAjax1, ajaxDelay);
|
||||
|
@ -12,34 +12,214 @@ function initSearch()
|
||||
if (sch != null) {
|
||||
var box = document.getElementById("searchbox");
|
||||
var cxl = document.getElementById("searchcancel");
|
||||
var radio = document.querySelectorAll('input[name=searchopt]');
|
||||
var remote = document.getElementById("remoteresults");
|
||||
var msgs = document.getElementById("snarkMessages");
|
||||
var main = document.getElementById("mainsection");
|
||||
var lower = document.getElementById("lowersection");
|
||||
|
||||
// cancel listener
|
||||
cxl.addEventListener("click", function(event) {
|
||||
if (box.value !== "") {
|
||||
box.value = "";
|
||||
requestAjax2(-1);
|
||||
}
|
||||
cxl.classList.add("disabled");
|
||||
if (remote != null)
|
||||
remote.classList.add("disabled");
|
||||
if (main != null)
|
||||
main.classList.remove("disabled");
|
||||
if (msgs != null)
|
||||
msgs.classList.remove("disabled");
|
||||
if (lower != null)
|
||||
lower.classList.remove("disabled");
|
||||
removeRemoteResults();
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// search listener
|
||||
box.addEventListener("input", function(event) {
|
||||
if (box.value !== "") {
|
||||
cxl.classList.remove("disabled");
|
||||
if (msgs != null)
|
||||
msgs.classList.add("disabled");
|
||||
if (lower != null)
|
||||
lower.classList.add("disabled");
|
||||
if (radio != null) {
|
||||
var opt = document.querySelector('input[name=searchopt]:checked').value;
|
||||
if (opt === "1") {
|
||||
if (remote != null)
|
||||
remote.classList.add("disabled");
|
||||
if (main != null)
|
||||
main.classList.remove("disabled");
|
||||
requestAjax2(-1);
|
||||
} else if (opt === "2") {
|
||||
if (remote != null) {
|
||||
if (box.value.length >= 3) {
|
||||
removeUnrelatedResults(box.value);
|
||||
remote.classList.remove("disabled");
|
||||
requestAjaxRemote();
|
||||
} else {
|
||||
remote.classList.add("disabled");
|
||||
}
|
||||
}
|
||||
if (main != null)
|
||||
main.classList.add("disabled");
|
||||
} else if (opt === "3") {
|
||||
if (remote != null) {
|
||||
if (box.value.length >= 3) {
|
||||
removeUnrelatedResults(box.value);
|
||||
remote.classList.remove("disabled");
|
||||
requestAjaxRemote();
|
||||
} else {
|
||||
remote.classList.add("disabled");
|
||||
}
|
||||
}
|
||||
if (main != null)
|
||||
main.classList.remove("disabled");
|
||||
requestAjax2(-1);
|
||||
}
|
||||
} else {
|
||||
if (remote != null)
|
||||
remote.classList.add("disabled");
|
||||
if (main != null)
|
||||
main.classList.remove("disabled");
|
||||
requestAjax2(-1);
|
||||
}
|
||||
} else {
|
||||
cxl.classList.add("disabled");
|
||||
}
|
||||
removeRemoteResults();
|
||||
if (remote != null)
|
||||
remote.classList.add("disabled");
|
||||
if (main != null)
|
||||
main.classList.remove("disabled");
|
||||
if (msgs != null)
|
||||
msgs.classList.remove("disabled");
|
||||
if (lower != null)
|
||||
lower.classList.remove("disabled");
|
||||
requestAjax2(-1);
|
||||
}
|
||||
});
|
||||
|
||||
// radio listener
|
||||
if (radio != null) {
|
||||
for (var index = 0; index < radio.length; index++)
|
||||
{
|
||||
var r = radio[index];
|
||||
r.addEventListener("click", function(event) {
|
||||
var opt = document.querySelector('input[name=searchopt]:checked').value;
|
||||
if (opt === "1" || box.value === "") {
|
||||
if (remote != null)
|
||||
remote.classList.add("disabled");
|
||||
if (main != null)
|
||||
main.classList.remove("disabled");
|
||||
//requestAjax2(-1);
|
||||
} else if (opt === "2") {
|
||||
if (remote != null) {
|
||||
if (box.value.length >= 3) {
|
||||
removeUnrelatedResults(box.value);
|
||||
remote.classList.remove("disabled");
|
||||
//requestAjaxRemote();
|
||||
} else {
|
||||
remote.classList.add("disabled");
|
||||
}
|
||||
}
|
||||
if (main != null)
|
||||
main.classList.add("disabled");
|
||||
} else if (opt === "3") {
|
||||
if (remote != null) {
|
||||
if (box.value.length >= 3) {
|
||||
removeUnrelatedResults(box.value);
|
||||
remote.classList.remove("disabled");
|
||||
//requestAjaxRemote();
|
||||
} else {
|
||||
remote.classList.add("disabled");
|
||||
}
|
||||
}
|
||||
if (main != null)
|
||||
main.classList.remove("disabled");
|
||||
//requestAjax2(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// initial setup
|
||||
if (box.value !== "") {
|
||||
cxl.classList.remove("disabled");
|
||||
if (msgs != null)
|
||||
msgs.classList.add("disabled");
|
||||
if (lower != null)
|
||||
lower.classList.add("disabled");
|
||||
if (radio != null) {
|
||||
var opt = document.querySelector('input[name=searchopt]:checked').value;
|
||||
if (opt === "1") {
|
||||
if (remote != null)
|
||||
remote.classList.add("disabled");
|
||||
if (main != null)
|
||||
main.classList.remove("disabled");
|
||||
} else if (opt === "2") {
|
||||
if (remote != null) {
|
||||
if (box.value.length >= 3) {
|
||||
remote.classList.remove("disabled");
|
||||
} else {
|
||||
remote.classList.add("disabled");
|
||||
}
|
||||
}
|
||||
if (main != null)
|
||||
main.classList.add("disabled");
|
||||
} else if (opt === "3") {
|
||||
if (remote != null) {
|
||||
if (box.value.length >= 3) {
|
||||
remote.classList.remove("disabled");
|
||||
} else {
|
||||
remote.classList.add("disabled");
|
||||
}
|
||||
}
|
||||
if (main != null)
|
||||
main.classList.remove("disabled");
|
||||
}
|
||||
} else {
|
||||
if (remote != null)
|
||||
remote.classList.add("disabled");
|
||||
if (main != null)
|
||||
main.classList.remove("disabled");
|
||||
}
|
||||
} else {
|
||||
cxl.classList.add("disabled");
|
||||
if (remote != null)
|
||||
remote.classList.add("disabled");
|
||||
if (main != null)
|
||||
main.classList.remove("disabled");
|
||||
if (msgs != null)
|
||||
msgs.classList.remove("disabled");
|
||||
if (lower != null)
|
||||
lower.classList.remove("disabled");
|
||||
}
|
||||
// so we don't get the link popup
|
||||
cxl.removeAttribute("href");
|
||||
}
|
||||
}
|
||||
|
||||
function removeUnrelatedResults(cur) {
|
||||
var footer = document.getElementById("remoteresultfooter");
|
||||
if (footer != null) {
|
||||
var prev = footer.getAttribute("search");
|
||||
if (prev != null) {
|
||||
if (!prev.startsWith(cur) && !cur.startsWith(prev)) {
|
||||
// unrelated search, clear previous results
|
||||
removeRemoteResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeRemoteResults() {
|
||||
var results = document.getElementById("removeresulttablebody");
|
||||
if (results != null)
|
||||
results.innerHTML = "";
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
initSearch();
|
||||
}, true);
|
||||
|
@ -265,6 +265,10 @@ _:-ms-lang(x), .snarkNav:last-child[href="/i2psnark/"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#remoteresults.disabled, #mainsection.disabled, #lowersection.disabled, #snarkMessages.disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* end topnav */
|
||||
|
||||
/* screenlogger */
|
||||
|
Reference in New Issue
Block a user