/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.clients.http;
import java.io.IOException;
import java.net.URI;
import freenet.client.HighLevelSimpleClient;
import freenet.config.Config;
import freenet.config.ConfigCallback;
import freenet.config.EnumerableOptionCallback;
import freenet.config.InvalidConfigValueException;
import freenet.config.NodeNeedRestartException;
import freenet.config.Option;
import freenet.config.SubConfig;
import freenet.config.WrapperConfig;
import freenet.l10n.NodeL10n;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.ProgramDirectory;
import freenet.node.useralerts.AbstractUserAlert;
import freenet.node.useralerts.UserAlert;
import freenet.pluginmanager.FredPluginConfigurable;
import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.MultiValueTable;
import freenet.support.Logger.LogLevel;
import freenet.support.URLEncoder;
import freenet.support.api.BooleanCallback;
import freenet.support.api.HTTPRequest;
/**
* Node Configuration Toadlet. Accessible from <code>http://.../config/</code>.
*/
// FIXME: add logging, comments
public class ConfigToadlet extends Toadlet implements LinkEnabledCallback {
// If a setting has to be more than a meg, something is seriously wrong!
private static final int MAX_PARAM_VALUE_SIZE = 1024 * 1024;
private String directoryBrowserPath;
private final SubConfig subConfig;
private final Config config;
private final NodeClientCore core;
private final Node node;
/** plugin is always null except when this ConfigToadlet serves a plugin */
private final FredPluginConfigurable plugin;
private boolean needRestart = false;
private NeedRestartUserAlert needRestartUserAlert;
/**
* Prompt for node restart
*/
private class NeedRestartUserAlert extends AbstractUserAlert {
private final String formPassword;
public NeedRestartUserAlert(String formPassword) {
this.formPassword = formPassword;
}
@Override
public String getTitle() {
return l10n("needRestartTitle");
}
@Override
public String getText() {
return getHTMLText().toString();
}
@Override
public String getShortText() {
return l10n("needRestartShort");
}
@Override
public HTMLNode getHTMLText() {
HTMLNode alertNode = new HTMLNode("div");
alertNode.addChild("#", l10n("needRestart"));
if (node.isUsingWrapper()) {
alertNode.addChild("br");
HTMLNode restartForm = alertNode.addChild(
"form",
new String[] { "action", "method", "enctype", "id",
"accept-charset" },
new String[] { "/", "post", "multipart/form-data",
"restartForm", "utf-8" }).addChild("div");
restartForm.addChild("input", new String[] { "type", "name",
"value" }, new String[] { "hidden", "formPassword",
formPassword });
restartForm.addChild("div");
restartForm.addChild("input",//
new String[] { "type", "name" },//
new String[] { "hidden", "restart" });
restartForm.addChild("input", //
new String[] { "type", "name", "value" },//
new String[] { "submit", "restart2",
l10n("restartNode") });
}
return alertNode;
}
@Override
public short getPriorityClass() {
return UserAlert.WARNING;
}
@Override
public boolean isValid() {
return needRestart;
}
@Override
public boolean userCanDismiss() {
return false;
}
}
/**
* Describes which UI element should be used to present an option.
*/
private enum OptionType {
/**
* A writable option with an enumerable list of possible values.
*/
DROP_DOWN("dropdown"),
/**
* A writable option which can be either true or false.
*/
BOOLEAN("boolean"),
/**
* A writable option which is a path to a directory.
*/
DIRECTORY("directory"),
/**
* A writable option set with a string of text.
*/
TEXT("text"),
/**
* A read-only option presented in a text field.
*/
TEXT_READ_ONLY("text readonly");
/**
* A CSS class descriptor for this option type.
*/
public final String cssClass;
private OptionType(String cssClass) {
this.cssClass = cssClass;
}
}
public ConfigToadlet(String directoryBrowserPath,
HighLevelSimpleClient client, Config conf, SubConfig subConfig,
Node node, NodeClientCore core) {
this(directoryBrowserPath, client, conf, subConfig, node, core, null);
}
public ConfigToadlet(HighLevelSimpleClient client, Config conf,
SubConfig subConfig, Node node, NodeClientCore core) {
this(client, conf, subConfig, node, core, null);
}
public ConfigToadlet(String directoryBrowserPath,
HighLevelSimpleClient client, Config conf, SubConfig subConfig,
Node node, NodeClientCore core, FredPluginConfigurable plugin) {
this(client, conf, subConfig, node, core, plugin);
this.directoryBrowserPath = directoryBrowserPath;
}
public ConfigToadlet(HighLevelSimpleClient client, Config conf,
SubConfig subConfig, Node node, NodeClientCore core,
FredPluginConfigurable plugin) {
super(client);
config = conf;
this.core = core;
this.node = node;
this.subConfig = subConfig;
this.plugin = plugin;
this.directoryBrowserPath = "/unset-browser-path/";
}
public void handleMethodPOST(URI uri, HTTPRequest request,
ToadletContext ctx) throws ToadletContextClosedException,
IOException, RedirectException {
if(!ctx.checkFullAccess(this))
return;
// User requested reset to defaults, so present confirmation page.
if (request.isPartSet("confirm-reset-to-defaults")) {
PageNode page = ctx.getPageMaker().getPageNode(
l10n("confirmResetTitle"), ctx);
HTMLNode pageNode = page.outer;
HTMLNode contentNode = page.content;
HTMLNode content = ctx.getPageMaker().getInfobox("infobox-warning",
l10n("confirmResetTitle"), contentNode, "reset-confirm",
true);
content.addChild("#", l10n("confirmReset"));
HTMLNode formNode = ctx.addFormChild(content, path(), "yes-button");
String subconfig = request.getPartAsStringFailsafe("subconfig",
MAX_PARAM_VALUE_SIZE);
formNode.addChild("input",
new String[] { "type", "name", "value" }, new String[] {
"hidden", "subconfig", subconfig });
// Persist visible fields so that they are reset to default or
// unsaved changes are persisted.
for (String part : request.getParts()) {
if (part.startsWith(subconfig)) {
formNode.addChild(
"input",
new String[] { "type", "name", "value" },
new String[] {
"hidden",
part,
request.getPartAsStringFailsafe(part,
MAX_PARAM_VALUE_SIZE) });
}
}
formNode.addChild("input",
new String[] { "type", "name", "value" }, new String[] {
"submit", "reset-to-defaults",
NodeL10n.getBase().getString("Toadlet.yes") });
formNode.addChild("input",
new String[] { "type", "name", "value" }, new String[] {
"submit", "decline-default-reset",
NodeL10n.getBase().getString("Toadlet.no") });
writeHTMLReply(ctx, 200, "OK", pageNode.generate());
return;
}
// Returning from directory selector with a selection or declining
// resetting settings to defaults.
// Re-render config page with any changes made in the selector and/or
// persisting values changed but
// not applied.
if (request.isPartSet(LocalFileBrowserToadlet.selectDir)
|| request.isPartSet("decline-default-reset")) {
handleMethodGET(uri, request, ctx);
return;
}
// Entering directory selector from config page.
// This would be two loops if it checked for a redirect
// (key.startsWith("select-directory.")) before
// constructing params string. It always constructs it, then redirects
// if it turns out to be needed.
boolean directorySelector = false;
StringBuilder paramsBuilder = new StringBuilder();
paramsBuilder.append('?');
String value;
for (String key : request.getParts()) {
// Prepare parts for page selection redirect:
// Extract option and put into "select-for"; preserve others.
value = request.getPartAsStringFailsafe(key, MAX_PARAM_VALUE_SIZE);
if (key.startsWith("select-directory.")) {
paramsBuilder
.append("select-for=")
.append(URLEncoder.encode(key.substring("select-directory.".length()), true))
.append('&');
directorySelector = true;
} else {
paramsBuilder.append(URLEncoder.encode(key, true)).append('=')
.append(URLEncoder.encode(value, true)).append('&');
}
}
String params = paramsBuilder.toString();
if (directorySelector) {
MultiValueTable<String, String> headers = new MultiValueTable<String, String>(
1);
// params ends in &. Download directory browser starts in default
// download directory.
headers.put("Location", directoryBrowserPath + params + "path="
+ core.getDownloadsDir().getAbsolutePath());
ctx.sendReplyHeaders(302, "Found", headers, null, 0);
return;
}
StringBuilder errbuf = new StringBuilder();
boolean logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
String prefix = request.getPartAsStringFailsafe("subconfig",
MAX_PARAM_VALUE_SIZE);
if (logMINOR) {
Logger.minor(this, "Current config prefix is " + prefix);
}
boolean resetToDefault = request.isPartSet("reset-to-defaults");
if (resetToDefault && logMINOR) {
Logger.minor(this, "Resetting to defaults");
}
for (Option<?> o : config.get(prefix).getOptions()) {
String configName = o.getName();
if (logMINOR) {
Logger.minor(this, "Checking option " + prefix + '.'
+ configName);
}
// This ignores unrecognized parameters.
if (request.isPartSet(prefix + '.' + configName)) {
// Current subconfig is to be reset to default.
if (resetToDefault) {
// Disallow resetting fproxy port number to default as it
// might break the link to start fproxy on the system tray,
// shortcuts etc.
if (prefix.equals("fproxy") && configName.equals("port"))
continue;
value = o.getDefault();
} else {
value = request.getPartAsStringFailsafe(prefix + '.'
+ configName, MAX_PARAM_VALUE_SIZE);
}
if (!(o.getValueDisplayString().equals(value))) {
if (logMINOR) {
Logger.minor(this, "Changing " + prefix + '.'
+ configName + " to " + value);
}
try {
o.setValue(value);
} catch (InvalidConfigValueException e) {
errbuf.append(o.getName()).append(' ')
.append(e.getMessage()).append('\n');
} catch (NodeNeedRestartException e) {
needRestart = true;
} catch (Exception e) {
errbuf.append(o.getName()).append(' ').append(e)
.append('\n');
Logger.error(this, "Caught " + e, e);
}
} else if (logMINOR) {
Logger.minor(this, prefix + '.' + configName
+ " not changed");
}
}
}
// Wrapper params
String wrapperConfigName = "wrapper.java.maxmemory";
if (request.isPartSet(wrapperConfigName)) {
value = request.getPartAsStringFailsafe(wrapperConfigName,
MAX_PARAM_VALUE_SIZE);
if (!WrapperConfig.getWrapperProperty(wrapperConfigName).equals(
value)) {
if (logMINOR) {
Logger.minor(this, "Setting " + wrapperConfigName + " to "
+ value);
}
WrapperConfig.setWrapperProperty(wrapperConfigName, value);
}
}
config.store();
PageNode page = ctx.getPageMaker().getPageNode(l10n("appliedTitle"),
ctx);
HTMLNode pageNode = page.outer;
HTMLNode contentNode = page.content;
if (errbuf.length() == 0) {
HTMLNode content = ctx.getPageMaker().getInfobox("infobox-success",
l10n("appliedTitle"), contentNode, "configuration-applied",
true);
content.addChild("#", l10n("appliedSuccess"));
if (needRestart) {
content.addChild("br");
content.addChild("#", l10n("needRestart"));
if (node.isUsingWrapper()) {
content.addChild("br");
HTMLNode restartForm = ctx.addFormChild(content, "/",
"restartForm");
restartForm.addChild("input",//
new String[] { "type", "name" },//
new String[] { "hidden", "restart" });
restartForm.addChild("input", //
new String[] { "type", "name", "value" },//
new String[] { "submit", "restart2",//
l10n("restartNode") });
}
if (needRestartUserAlert == null) {
needRestartUserAlert = new NeedRestartUserAlert(ctx.getFormPassword());
ctx.getAlertManager().register(needRestartUserAlert);
}
}
} else {
HTMLNode content = ctx
.getPageMaker()
.getInfobox("infobox-error", l10n("appliedFailureTitle"),
contentNode, "configuration-error", true)
.addChild("div", "class", "infobox-content");
content.addChild("#", l10n("appliedFailureExceptions"));
content.addChild("br");
content.addChild("#", errbuf.toString());
}
HTMLNode content = ctx.getPageMaker().getInfobox("infobox-normal",
l10n("possibilitiesTitle"), contentNode,
"configuration-possibilities", false);
content.addChild("a", new String[] { "href", "title" }, new String[] {
path(), l10n("shortTitle") }, l10n("returnToNodeConfig"));
content.addChild("br");
addHomepageLink(content);
writeHTMLReply(ctx, 200, "OK", pageNode.generate());
}
private static String l10n(String string) {
return NodeL10n.getBase().getString("ConfigToadlet." + string);
}
public void handleMethodGET(URI uri, HTTPRequest req, ToadletContext ctx)
throws ToadletContextClosedException, IOException {
if(!ctx.checkFullAccess(this))
return;
boolean advancedModeEnabled = ctx.isAdvancedModeEnabled();
PageNode page = ctx.getPageMaker().getPageNode(
NodeL10n.getBase().getString("ConfigToadlet.fullTitle"), ctx);
HTMLNode pageNode = page.outer;
HTMLNode contentNode = page.content;
contentNode.addChild(ctx.getAlertManager().createSummary());
HTMLNode infobox = contentNode.addChild("div", "class",
"infobox infobox-normal");
infobox.addChild("div", "class", "infobox-header", l10n("title"));
HTMLNode configNode = infobox.addChild("div", "class",
"infobox-content");
HTMLNode formNode = ctx.addFormChild(configNode, path(), "configForm");
// Invisible apply button at the top so that an enter keypress will
// apply settings instead of
// going to a directory browser if present.
formNode.addChild("input", new String[] { "type", "value", "class" },
new String[] { "submit", l10n("apply"), "invisible" });
/*
* Special case: present an option for the wrapper's maximum memory
* under Core configuration, provided the maximum memory property is
* defined. (the wrapper is being used)
*/
if (subConfig.getPrefix().equals("node")
&& WrapperConfig.canChangeProperties()) {
String configName = "wrapper.java.maxmemory";
String curValue = WrapperConfig.getWrapperProperty(configName);
// If persisted from directory browser, override. This is a POST
// HTTPRequest.
if (req.isPartSet(configName)) {
curValue = req.getPartAsStringFailsafe(configName,
MAX_PARAM_VALUE_SIZE);
}
if (curValue != null) {
formNode.addChild("div", "class", "configprefix",
l10n("wrapper"));
HTMLNode list = formNode.addChild("ul", "class", "config");
HTMLNode item = list.addChild("li", "class",
OptionType.TEXT.cssClass);
// FIXME how to get the real default???
String defaultValue = "256";
item.addChild(
"span",
new String[] { "class", "title", "style" },
new String[] {
"configshortdesc",
NodeL10n.getBase().getString(
"ConfigToadlet.defaultIs",
new String[] { "default" },
new String[] { defaultValue }),
"cursor: help;" }).addChild(
NodeL10n.getBase().getHTMLNode(
"WrapperConfig." + configName + ".short"));
item.addChild("span", "class", "config")
.addChild(
"input",
new String[] { "type", "class", "name", "value" },
new String[] { "text", "config", configName,
curValue });
item.addChild("span", "class", "configlongdesc").addChild(
NodeL10n.getBase().getHTMLNode(
"WrapperConfig." + configName + ".long"));
}
}
short displayedConfigElements = 0;
HTMLNode configGroupUlNode = new HTMLNode("ul", "class", "config");
String overriddenOption = null;
String overriddenValue = null;
// A value changed by the directory selector takes precedence.
if (req.isPartSet("select-for")
&& req.isPartSet(LocalFileBrowserToadlet.selectDir)) {
overriddenOption = req.getPartAsStringFailsafe("select-for",
MAX_PARAM_VALUE_SIZE);
overriddenValue = req.getPartAsStringFailsafe("filename",
MAX_PARAM_VALUE_SIZE);
}
/*
* Present all other options for this subconfig.
*/
for (Option<?> o : subConfig.getOptions()) {
if (!((!advancedModeEnabled) && o.isExpert())) {
displayedConfigElements++;
String configName = o.getName();
String fullName = subConfig.getPrefix() + '.' + configName;
String value = o.getValueDisplayString();
if (value == null) {
Logger.error(this, fullName
+ "has returned null from config!);");
continue;
}
ConfigCallback<?> callback = o.getCallback();
final OptionType optionType;
if (callback instanceof EnumerableOptionCallback) {
optionType = OptionType.DROP_DOWN;
} else if (callback instanceof BooleanCallback) {
optionType = OptionType.BOOLEAN;
} else if (callback instanceof ProgramDirectory.DirectoryCallback
&& !callback.isReadOnly()) {
optionType = OptionType.DIRECTORY;
} else if (!callback.isReadOnly()) {
optionType = OptionType.TEXT;
} else /* if (callback.isReadOnly()) */{
optionType = OptionType.TEXT_READ_ONLY;
}
// If ConfigToadlet is serving a plugin, ask the plugin to
// translate the
// config descriptions, otherwise use the node's BaseL10n
// instance like
// normal.
HTMLNode shortDesc = o.getShortDescNode(plugin);
HTMLNode longDesc = o.getLongDescNode(plugin);
HTMLNode configItemNode = configGroupUlNode.addChild("li");
String defaultValue;
if (callback instanceof BooleanCallback) {
// Only case where values are localised.
defaultValue = l10n(Boolean
.toString(Boolean.valueOf(value)));
} else {
defaultValue = o.getDefault();
}
configItemNode.addAttribute("class", optionType.cssClass);
configItemNode
.addChild("a", new String[] { "name", "id" },
new String[] { configName, configName })
.addChild(
"span",
new String[] { "class", "title", "style" },
new String[] {
"configshortdesc",
NodeL10n.getBase().getString(
"ConfigToadlet.defaultIs",
new String[] { "default" },
new String[] { defaultValue })
+ (advancedModeEnabled ? " ["
+ fullName + ']' : ""),
"cursor: help;" }).addChild(shortDesc);
HTMLNode configItemValueNode = configItemNode.addChild("span",
"class", "config");
// Values persisted through browser or backing down from
// resetting to defaults
// override the currently applied ones.
if (req.isPartSet(fullName)) {
value = req.getPartAsStringFailsafe(fullName,
MAX_PARAM_VALUE_SIZE);
}
if (overriddenOption != null
&& overriddenOption.equals(fullName))
value = overriddenValue;
switch (optionType) {
case DROP_DOWN:
configItemValueNode.addChild(addComboBox(value,
(EnumerableOptionCallback) callback, fullName,
callback.isReadOnly()));
break;
case BOOLEAN:
configItemValueNode.addChild(addBooleanComboBox(
Boolean.valueOf(value), fullName,
callback.isReadOnly()));
break;
case DIRECTORY:
configItemValueNode.addChild(addTextBox(value, fullName, o,
false));
configItemValueNode.addChild(
"input",
new String[] { "type", "name", "value" },
new String[] {
"submit",
"select-directory." + fullName,
NodeL10n.getBase().getString(
"QueueToadlet.browseToChange") });
break;
case TEXT_READ_ONLY:
configItemValueNode.addChild(addTextBox(value, fullName, o,
true));
break;
case TEXT:
configItemValueNode.addChild(addTextBox(value, fullName, o,
false));
break;
}
configItemNode.addChild("span", "class", "configlongdesc")
.addChild(longDesc);
}
}
if (displayedConfigElements > 0) {
formNode.addChild(
"div",
"class",
"configprefix",
(plugin == null) ? l10n(subConfig.getPrefix()) : plugin
.getString(subConfig.getPrefix()));
formNode.addChild("a", "id", subConfig.getPrefix());
formNode.addChild(configGroupUlNode);
}
formNode.addChild("input", new String[] { "type", "value" },
new String[] { "submit", l10n("apply") });
formNode.addChild("input", new String[] { "type", "value" },
new String[] { "reset", l10n("undo") });
formNode.addChild("input", new String[] { "type", "name", "value" },
new String[] { "hidden", "subconfig", subConfig.getPrefix() });
// 'Node' prefix options should not be reset to defaults as it is a,
// quoting Toad, "very bad idea".
// Options whose defaults are not wise to apply include the location of
// the master keys file,
// the Darknet port number, and the datastore size.
if (!subConfig.getPrefix().equals("node")) {
formNode.addChild("input",
new String[] { "type", "name", "value" }, new String[] {
"submit", "confirm-reset-to-defaults",
l10n("resetToDefaults") });
}
this.writeHTMLReply(ctx, 200, "OK", pageNode.generate());
}
/**
* Generates a text box for the given setting suitable for adding to an
* existing form.
*
* @param value
* The current value of the option. It is displayed in the text
* box.
* @param fullName
* The full name of the option, used to name the text field.
* @param o
* The option, used to add the short description as an "alt"
* attribute.
* @param disabled
* Whether the text box should be disabled.
* @return An input of type "text" and class "config" containing the current
* value of the option.
*/
public static HTMLNode addTextBox(String value, String fullName,
Option<?> o, boolean disabled) {
HTMLNode result;
if (disabled) {
result = new HTMLNode("input", new String[] { "type", "class",
"disabled", "alt", "name", "value" }, //
new String[] { "text", "config", "disabled",
o.getShortDesc(), fullName, value });
} else {
result = new HTMLNode("input", new String[] { "type", "class",
"alt", "name", "value" }, //
new String[] { "text", "config", o.getShortDesc(),
fullName, value });
}
return result;
}
/**
* Generates a drop-down combobox for the given enumerable option suitable
* for adding to an existing form. Its first element is the "select"
* element, so any Javascript attributes can be added to the output.
*
* @param value
* The currently applied value of the option.
* @param o
* The option, used to list all values.
* @param fullName
* The full name of the option, used to name the drop-down.
* @param disabled
* Whether the drop-down should be disabled.
* @return An HTMLNode of a "select" with "option" children for each of the
* possible values. If the value specified in value is one of the
* options, it will be selected.
*/
public static HTMLNode addComboBox(String value,
EnumerableOptionCallback o, String fullName, boolean disabled) {
HTMLNode result;
if (disabled) {
result = new HTMLNode("select", //
new String[] { "name", "disabled" }, //
new String[] { fullName, "disabled" });
} else {
result = new HTMLNode("select", "name", fullName);
}
for (String possibleValue : o.getPossibleValues()) {
if (possibleValue.equals(value)) {
result.addChild("option", new String[] { "value", "selected" },
new String[] { possibleValue, "selected" },
possibleValue);
} else {
result.addChild("option", "value", possibleValue, possibleValue);
}
}
return result;
}
/**
* Generates a drop-down combobox for a true/false option suitable for
* adding to an existing form. Its first element is the "select" element, so
* any Javascript attributes can be added to the output.
*
* @param value
* The current value of the option. This will be selected.
* @param fullName
* The full name of the option, used to name the drop-down.
* @param disabled
* Whether the drop-down should be disabled.
* @return An HTMLNode of a "select" with an "option" child for localized
* "true" and "false", with the current value selected.
*/
public static HTMLNode addBooleanComboBox(boolean value, String fullName,
boolean disabled) {
HTMLNode result;
if (disabled) {
result = new HTMLNode("select", //
new String[] { "name", "disabled" }, //
new String[] { fullName, "disabled" });
} else {
result = new HTMLNode("select", "name", fullName);
}
if (value) {
result.addChild("option", new String[] { "value", "selected" },
new String[] { "true", "selected" }, l10n("true"));
result.addChild("option", "value", "false", l10n("false"));
} else {
result.addChild("option", "value", "true", l10n("true"));
result.addChild("option", new String[] { "value", "selected" },
new String[] { "false", "selected" }, l10n("false"));
}
return result;
}
@Override
public String path() {
return "/config/" + subConfig.getPrefix();
}
@Override
public boolean isEnabled(ToadletContext ctx) {
Option<?>[] o = subConfig.getOptions();
if (ctx.isAdvancedModeEnabled())
return true;
for (Option<?> option : o)
if (!option.isExpert())
return true;
return false;
}
}