package net.i2p.router.web; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; import net.i2p.servlet.RequestWrapper; import net.i2p.util.Log; /** * Simple form handler base class - does not depend on servlets or jsp, * but instead the subclasses are populated with javabean properties. e.g. * <jsp:setProperty name="handler" property="*" /> * * The form is "processed" after the properties are set and the first output * property is retrieved - either getAll(), getNotices() or getErrors(). * */ public abstract class FormHandler { protected RouterContext _context; protected Log _log; /** Not for multipart/form-data, will be null */ @SuppressWarnings("rawtypes") protected Map _settings; /** Only for multipart/form-data. Warning, parameters are NOT XSS filtered */ protected RequestWrapper _requestWrapper; private String _nonce, _nonce1, _nonce2; protected String _action; protected String _method; private final List<String> _errors; private final List<String> _notices; private boolean _processed; private boolean _valid; public FormHandler() { _errors = new ArrayList<String>(); _notices = new ArrayList<String>(); _valid = true; } /** * Configure this bean to query a particular router context * * @param contextId beginning few characters of the routerHash, or null to pick * the first one we come across. */ public void setContextId(String contextId) { try { _context = ContextHelper.getContext(contextId); _log = _context.logManager().getLog(getClass()); } catch (Throwable t) { t.printStackTrace(); } } public void setNonce(String val) { _nonce = val == null ? null : DataHelper.stripHTML(val); } public void setAction(String val) { _action = val == null ? null : DataHelper.stripHTML(val); } /** * For many forms, it's easiest just to put all the parameters here. * * @since 0.9.4 consolidated from numerous FormHandlers */ @SuppressWarnings({"rawtypes", "unchecked"}) public void setSettings(Map settings) { _settings = new HashMap(settings); } /** * Only set by formhandler.jsi for multipart/form-data * * @since 0.9.19 */ public void setRequestWrapper(RequestWrapper rw) { _requestWrapper = rw; } /** * Same as HelperBase * @since 0.9.14.1 */ public boolean isAdvanced() { return _context.getBooleanProperty(HelperBase.PROP_ADVANCED); } /** * setSettings() must have been called previously * Curses Jetty for returning arrays. * * @since 0.9.4 consolidated from numerous FormHandlers */ protected String getJettyString(String key) { if (_settings == null) return null; String[] arr = (String[]) _settings.get(key); if (arr == null) return null; return arr[0].trim(); } /** * Call this to prevent changes using GET * * @param val the request method * @since 0.8.2 */ public void storeMethod(String val) { _method = val; } /** * The old nonces from the session * @since 0.9.4 */ public void storeNonces(String n1, String n2) { _nonce1 = n1; _nonce2 = n2; } /** * Override this to perform the final processing (in turn, adding formNotice * and formError messages, etc) * */ protected void processForm() {} /** * Add an error message to display * Use if it does not include a link. * Escapes '<' and '>' before queueing */ protected void addFormError(String errorMsg) { if (errorMsg == null) return; _errors.add(DataHelper.escapeHTML(errorMsg)); } /** * Add a non-error message to display * Use if it does not include a link. * Escapes '<' and '>' before queueing */ protected void addFormNotice(String msg) { if (msg == null) return; _notices.add(DataHelper.escapeHTML(msg)); } /** * Add a non-error message to display * Use if it includes a link or other formatting. * Does not escape '<' and '>' before queueing * @since 0.9.14.1 */ protected void addFormNoticeNoEscape(String msg) { if (msg == null) return; _notices.add(msg); } /** * Add an error message to display * Use if it includes a link or other formatting. * Does not escape '<' and '>' before queueing * @since 0.9.19 */ protected void addFormErrorNoEscape(String msg) { if (msg == null) return; _errors.add(msg); } /** * Display everything, wrap it in a div for consistent presentation * */ public String getAllMessages() { validate(); process(); if (_errors.isEmpty() && _notices.isEmpty()) return ""; StringBuilder buf = new StringBuilder(512); buf.append("<div class=\"messages\" id=\"messages\">"); if (!_errors.isEmpty()) { buf.append("<div class=\"error\">"); buf.append(render(_errors)); buf.append("</div>"); } if (!_notices.isEmpty()) { buf.append("<div class=\"notice\">"); buf.append(render(_notices)); buf.append("</div>"); } buf.append("</div>"); return buf.toString(); } /** * Display any error messages (processing the form if it hasn't * been yet) * */ public String getErrors() { validate(); process(); return render(_errors); } /** * Display any non-error messages (processing the form if it hasn't * been yet) * */ public String getNotices() { validate(); process(); return render(_notices); } /** * Make sure the nonce was set correctly, otherwise someone could just * create a link like /confignet.jsp?hostname=localhost and break the * user's node (or worse). * */ private void validate() { if (_processed) return; _valid = true; if (_action == null) { // not a form submit _valid = false; return; } // To prevent actions with GET, jsps must call storeMethod() if (_method != null && !"POST".equals(_method)) { addFormError("Invalid form submission, requires POST"); _valid = false; return; } // If passwords are turned on, all is assumed good if (_context.getBooleanProperty(RouterConsoleRunner.PROP_PW_ENABLE)) { _valid = true; return; } if (_nonce == null) { //addFormError("You trying to mess with me? Huh? Are you?"); _valid = false; return; } String sharedNonce = CSSHelper.getNonce(); if (sharedNonce.equals(_nonce)) { return; } if (!_nonce.equals(_nonce1) && !_nonce.equals(_nonce2)) { addFormError(_t("Invalid form submission, probably because you used the 'back' or 'reload' button on your browser. Please resubmit.") + ' ' + _t("If the problem persists, verify that you have cookies enabled in your browser.")); _valid = false; } } private void process() { if (!_processed) { if (_valid) processForm(); _processed = true; } } private static String render(List<String> source) { if (source.isEmpty()) { return ""; } else { StringBuilder buf = new StringBuilder(512); buf.append("<ul>\n"); for (int i = 0; i < source.size(); i++) { buf.append("<li>"); buf.append(source.get(i)); buf.append("</li>\n"); } buf.append("</ul>\n"); return buf.toString(); } } /** * Generate a new nonce. * Only call once per page! * @return a new random long as a String * @since 0.8.5 */ public String getNewNonce() { String rv = Long.toString(_context.random().nextLong()); return rv; } /** translate a string */ public String _t(String s) { return Messages.getString(s, _context); } /** * translate a string with a parameter * This is a lot more expensive than _t(s), so use sparingly. * * @param s string to be translated containing {0} * The {0} will be replaced by the parameter. * Single quotes must be doubled, i.e. ' -> '' in the string. * @param o parameter, not translated. * To translate parameter also, use _t("foo {0} bar", _t("baz")) * Do not double the single quotes in the parameter. * Use autoboxing to call with ints, longs, floats, etc. */ public String _t(String s, Object o) { return Messages.getString(s, o, _context); } /** two params @since 0.8.2 */ public String _t(String s, Object o, Object o2) { return Messages.getString(s, o, o2, _context); } /** * Mark a string for extraction by xgettext and translation. * Use this only in static initializers. * It does not translate! * @return s */ public static String _x(String s) { return s; } }