diff --git a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java index 1ad16eb111bdc8511b71f79483712df024fec61d..3d2919f396e8cbdfa16eba8164a824c637d63c44 100644 --- a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java +++ b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java @@ -188,6 +188,7 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp { PluginUpdateHandler puh = new PluginUpdateHandler(_context, this); register((Checker)puh, PLUGIN, HTTP, 0); register((Updater)puh, PLUGIN, HTTP, 0); + register((Updater)puh, PLUGIN, FILE, 0); // Don't do this until we can prevent it from retrying the same thing again... // handled inside P.U.H. for now //register((Updater)puh, PLUGIN, FILE, 0); @@ -523,7 +524,8 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp { UpdateItem item = new UpdateItem(PLUGIN, name); VersionAvailable va = _available.get(item); if (va == null) { - va = new VersionAvailable("", "", HTTP, uris); + UpdateMethod method = "file".equals(uri.getScheme()) ? FILE : HTTP; + va = new VersionAvailable("", "", method, uris); _available.putIfAbsent(item, va); } if (_log.shouldLog(Log.WARN)) @@ -971,8 +973,8 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp { * @param t may be null */ public void notifyTaskFailed(UpdateTask task, String reason, Throwable t) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Failed " + task + " for " + task.getType() + ": " + reason, t); + if (_log.shouldLog(Log.ERROR)) + _log.error("Failed " + task + " for " + task.getType() + ": " + reason, t); List<RegisteredUpdater> toTry = _downloaders.get(task); if (toTry != null) { UpdateItem ui = new UpdateItem(task.getType(), task.getID()); @@ -988,8 +990,27 @@ public class ConsoleUpdateManager implements UpdateManager, RouterApp { _downloaders.remove(task); _activeCheckers.remove(task); // any other types that shouldn't display? - if (task.getURI() != null && task.getType() != TYPE_DUMMY) - finishStatus("<b>" + _("Transfer failed from {0}", linkify(task.getURI().toString())) + "</b>"); + if (task.getURI() != null && task.getType() != TYPE_DUMMY) { + StringBuilder buf = new StringBuilder(256); + buf.append("<b>"); + String uri = task.getURI().toString(); + if (uri.startsWith("file:") || task.getMethod() == FILE) { + uri = DataHelper.stripHTML(task.getURI().getPath()); + buf.append(_("Install failed from {0}", uri)); + } else { + buf.append(_("Transfer failed from {0}")); + } + if (reason != null && reason.length() > 0) { + buf.append("<br>"); + buf.append(reason); + } + if (t != null && t.getMessage() != null && t.getMessage().length() > 0) { + buf.append("<br>"); + buf.append(DataHelper.stripHTML(t.getMessage())); + } + buf.append("</b>"); + finishStatus(buf.toString()); + } } /** diff --git a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java index f327f739da95915bfd2fcc47be0bf2845d94e22a..b360ecb2f6b6cea856ee7815079a85d516e100bc 100644 --- a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java @@ -67,7 +67,8 @@ class PluginUpdateHandler implements Checker, Updater { public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources, String appName, String newVersion, long maxTime) { if (type != UpdateType.PLUGIN || - method != UpdateMethod.HTTP || updateSources.isEmpty()) + (method != UpdateMethod.HTTP && method != UpdateMethod.FILE) || + updateSources.isEmpty()) return null; Properties props = PluginStarter.pluginProperties(_context, appName); String oldVersion = props.getProperty("version"); diff --git a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java index 65e9f5584af90ab0cc28374b9307d59d54e3937b..9b7143ed52371b2cde6a40acd74e339307b8f976 100644 --- a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java @@ -82,16 +82,16 @@ class PluginUpdateRunner extends UpdateRunner { protected void update() { _updated = false; - if(_xpi2pURL.startsWith("file://")) { - updateStatus("<b>" + _("Attempting to install from file {0}", _xpi2pURL) + "</b>"); - // strip off "file://" - String xpi2pfile = _xpi2pURL.substring(7); - if(xpi2pfile.length() == 0) { - statusDone("<b>" + _("No file specified {0}", _xpi2pURL) + "</b>"); + if (_xpi2pURL.startsWith("file:") || _method == UpdateMethod.FILE) { + // strip off file:// or just file: + String xpi2pfile = _uri.getPath(); + if(xpi2pfile == null || xpi2pfile.length() == 0) { + statusDone("<b>" + _("Bad URL {0}", _xpi2pURL) + "</b>"); } else { // copy the contents of from to _updateFile long alreadyTransferred = (new File(xpi2pfile)).getAbsoluteFile().length(); if(FileUtil.copy((new File(xpi2pfile)).getAbsolutePath(), _updateFile, true, false)) { + updateStatus("<b>" + _("Attempting to install from file {0}", _xpi2pURL) + "</b>"); transferComplete(alreadyTransferred, alreadyTransferred, 0L, _xpi2pURL, _updateFile, false); } else { statusDone("<b>" + _("Failed to install from file {0}, copy failed.", _xpi2pURL) + "</b>"); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java index 729a4b14d1c73629cd39e0bcc281b037e0256a3a..d957938fff4ffbb6b7e1e309c9175a5152583034 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java @@ -1,6 +1,10 @@ package net.i2p.router.web; +import java.io.BufferedOutputStream; import java.io.File; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; @@ -13,11 +17,15 @@ import java.util.Set; import net.i2p.app.ClientApp; import net.i2p.app.ClientAppState; +import net.i2p.crypto.SU3File; +import net.i2p.crypto.TrustedUpdate; +import net.i2p.data.DataHelper; import net.i2p.router.client.ClientManagerFacadeImpl; import net.i2p.router.startup.ClientAppConfig; import net.i2p.router.startup.LoadClientAppsJob; import net.i2p.router.update.ConsoleUpdateManager; import static net.i2p.update.UpdateType.*; +import net.i2p.util.SecureFileOutputStream; import org.eclipse.jetty.server.handler.ContextHandlerCollection; @@ -66,6 +74,15 @@ public class ConfigClientsHandler extends FormHandler { addFormError("Plugins disabled"); return; } + if (_action.equals(_("Install Plugin from File"))) { + if (pluginsEnabled && + (_context.getBooleanPropertyDefaultTrue(ConfigClientsHelper.PROP_ENABLE_PLUGIN_INSTALL) || + isAdvanced())) + installPluginFromFile(); + else + addFormError("Plugins disabled"); + return; + } if (_action.equals(_("Update All Installed Plugins"))) { if (pluginsEnabled) updateAllPlugins(); @@ -388,6 +405,73 @@ public class ConfigClientsHandler extends FormHandler { installPlugin(null, url); } + /** + * @since 0.9.19 + */ + private void installPluginFromFile() { + InputStream in = _requestWrapper.getInputStream("pluginFile"); + // go to some trouble to verify it's an su3 or xpi2p file before + // passing it along, so we can display a good error message + byte[] su3Magic = DataHelper.getASCII(SU3File.MAGIC); + byte[] zipMagic = new byte[] { 0x50, 0x4b, 0x03, 0x04 }; + byte[] magic = new byte[TrustedUpdate.HEADER_BYTES + zipMagic.length]; + File tmp = null; + OutputStream out = null; + try { + // non-null but zero bytes if no file entered, don't know why + if (in == null || in.available() <= 0) { + addFormError(_("You must enter a file")); + return; + } + DataHelper.read(in, magic); + boolean isSU3 = DataHelper.eq(magic, 0, su3Magic, 0, su3Magic.length); + if (!isSU3) { + if (!DataHelper.eq(magic, TrustedUpdate.HEADER_BYTES, zipMagic, 0, zipMagic.length)) { + String name = _requestWrapper.getFilename("pluginFile"); + if (name == null) + name = "File"; + throw new IOException(name + " is not an xpi2p or su3 plugin"); + } + } + tmp = new File(_context.getTempDir(), "plugin-" + _context.random().nextInt() + (isSU3 ? ".su3" : ".xpi2p")); + out = new BufferedOutputStream(new SecureFileOutputStream(tmp)); + out.write(magic); + byte buf[] = new byte[16*1024]; + int read = 0; + while ( (read = in.read(buf)) != -1) { + out.write(buf, 0, read); + } + out.close(); + String url = tmp.toURI().toString(); + // threaded... TODO inline to get better result to UI? + installPlugin(null, url); + // above sleeps 1000, give it some more time? + // or check for complete? + ConsoleUpdateManager mgr = UpdateHandler.updateManager(_context); + if (mgr == null) + return; + for (int i = 0; i < 10; i++) { + if (!mgr.isUpdateInProgress(PLUGIN)) { + tmp.delete(); + break; + } + try { + Thread.sleep(1000); + } catch (InterruptedException ie) {} + } + String status = mgr.getStatus(); + if (status != null && status.length() > 0) + addFormNoticeNoEscape(status); + } catch (IOException ioe) { + addFormError(_("Install from file failed") + " - " + ioe.getMessage()); + } finally { + // it's really a ByteArrayInputStream but we'll play along... + if (in != null) + try { in.close(); } catch (IOException ioe) {} + if (out != null) try { out.close(); } catch (IOException ioe) {} + } + } + private void updatePlugin(String app) { Properties props = PluginStarter.pluginProperties(_context, app); String url = props.getProperty("updateURL.su3"); @@ -434,10 +518,14 @@ public class ConfigClientsHandler extends FormHandler { addFormError(_("Bad URL {0}", url)); return; } - if (mgr.installPlugin(app, uri)) - addFormNotice(_("Downloading plugin from {0}", url)); - else + if (mgr.installPlugin(app, uri)) { + if (url.startsWith("file:")) + addFormNotice(_("Installing plugin from {0}", uri.getPath())); + else + addFormNotice(_("Downloading plugin from {0}", url)); + } else { addFormError("Cannot install, check logs"); + } // So that update() will post a status to the summary bar before we reload try { Thread.sleep(1000); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ReseedBundler.java b/apps/routerconsole/java/src/net/i2p/router/web/ReseedBundler.java index 6e5f391814df86c36551f445d4bf61d8f1fd35d2..949f2f5dfab7f84bba0548cbe2cebaa5e8813d1f 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ReseedBundler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ReseedBundler.java @@ -130,6 +130,7 @@ class ReseedBundler { entry.setTime(ri.getPublished()); zip.putNextEntry(entry); ri.writeBytes(zip); + zip.closeEntry(); } } catch (DataFormatException dfe) { rv.delete(); diff --git a/apps/routerconsole/jsp/configclients.jsp b/apps/routerconsole/jsp/configclients.jsp index eed9bf0d20713e396dfe472d03c854a1cab92285..65f67daf506500b2516be7c204be130bfeefc8a6 100644 --- a/apps/routerconsole/jsp/configclients.jsp +++ b/apps/routerconsole/jsp/configclients.jsp @@ -115,7 +115,7 @@ input.default { width: 1px; height: 1px; visibility: hidden; } <form action="" method="POST"> <input type="hidden" name="nonce" value="<%=pageNonce%>" > <jsp:getProperty name="clientshelper" property="form3" /> -<hr><div class="formaction"> +<div class="formaction"> <input type="submit" class="cancel" name="foo" value="<%=intl._("Cancel")%>" /> <input type="submit" name="action" class="accept" value="<%=intl._("Save Plugin Configuration")%>" /> </div></form></div> @@ -123,43 +123,44 @@ input.default { width: 1px; height: 1px; visibility: hidden; } } // pluginUpdateEnabled if (clientshelper.isPluginInstallEnabled()) { %> -<h3><a name="plugin"></a><%=intl._("Plugin Installation")%></h3><p> +<h3><a name="plugin"></a><%=intl._("Plugin Installation from URL")%></h3><p> <%=intl._("Look for available plugins on {0}.", "<a href=\"http://plugins.i2p\">plugins.i2p</a>")%> <%=intl._("To install a plugin, enter the download URL:")%> </p> -<% - } // pluginInstallEnabled - if (clientshelper.isPluginInstallEnabled() || clientshelper.isPluginUpdateEnabled()) { -%> <div class="wideload"> <form action="configclients" method="POST"> <input type="hidden" name="nonce" value="<%=pageNonce%>" > -<% - if (clientshelper.isPluginInstallEnabled()) { -%> <p> <input type="text" size="60" name="pluginURL" > </p><hr><div class="formaction"> <input type="submit" name="action" class="default" value="<%=intl._("Install Plugin")%>" /> <input type="submit" class="cancel" name="foo" value="<%=intl._("Cancel")%>" /> <input type="submit" name="action" class="download" value="<%=intl._("Install Plugin")%>" /> -</div> -<% - } // pluginInstallEnabled -%> -</div> +</div></form></div> + + +<div class="wideload"> +<h3><a name="plugin"></a><%=intl._("Plugin Installation from File")%></h3> +<form action="configclients" method="POST" enctype="multipart/form-data" accept-charset="UTF-8"> +<input type="hidden" name="nonce" value="<%=pageNonce%>" > +<p><%=intl._("Install plugin from file.")%> +<br><%=intl._("Select xpi2p or su3 file")%> : +<input type="file" name="pluginFile" > +</p><hr><div class="formaction"> +<input type="submit" name="action" class="download" value="<%=intl._("Install Plugin from File")%>" /> +</div></form></div> <% - if (clientshelper.isPluginUpdateEnabled()) { + } // pluginInstallEnabled + if (clientshelper.isPluginUpdateEnabled()) { %> -<hr><div class="formaction"> +<h3><a name="plugin"></a><%=intl._("Update All Plugins")%></h3> +<div class="formaction"> +<form action="configclients" method="POST"> +<input type="hidden" name="nonce" value="<%=pageNonce%>" > <input type="submit" name="action" class="reload" value="<%=intl._("Update All Installed Plugins")%>" /> -</div> -<% - } // pluginUpdateEnabled -%> </form></div> <% - } // pluginInstallEnabled || pluginUpdateEnabled + } // pluginUpdateEnabled } // showPlugins %> </div></div></body></html> diff --git a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java index ada36171b35250235ca92624e7026a2bf46e6d7a..4df447af8516764775c5b96dbe260ed74a3f7324 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java +++ b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java @@ -1829,6 +1829,7 @@ public class WebMail extends HttpServlet ZipEntry entry = new ZipEntry( name ); zip.putNextEntry( entry ); zip.write( content.content, content.offset, content.length ); + zip.closeEntry(); zip.finish(); shown = true; } catch (IOException e) {