]> WPIA git - gigi.git/blob - src/org/cacert/gigi/output/template/Form.java
upd: use a more strict pattern for handling forms
[gigi.git] / src / org / cacert / gigi / output / template / Form.java
1 package org.cacert.gigi.output.template;
2
3 import java.io.IOException;
4 import java.io.PrintWriter;
5 import java.util.HashMap;
6 import java.util.Map;
7
8 import javax.servlet.http.HttpServletRequest;
9 import javax.servlet.http.HttpSession;
10
11 import org.cacert.gigi.GigiApiException;
12 import org.cacert.gigi.localisation.Language;
13 import org.cacert.gigi.pages.LoginPage;
14 import org.cacert.gigi.pages.Page;
15 import org.cacert.gigi.util.RandomToken;
16
17 /**
18  * A generic HTML-form that handles CSRF-token creation.
19  */
20 public abstract class Form implements Outputable {
21
22     public static class PermamentFormException extends RuntimeException {
23
24         public PermamentFormException(GigiApiException cause) {
25             super(cause);
26         }
27
28         @Override
29         public synchronized GigiApiException getCause() {
30             return (GigiApiException) super.getCause();
31         }
32     }
33
34     public static final String CSRF_FIELD = "csrf";
35
36     private static final String SUBMIT_EXCEPTION = "form-submit-exception";
37
38     private final String csrf;
39
40     private final String action;
41
42     /**
43      * Creates a new {@link Form}.
44      * 
45      * @param hsr
46      *            the request to register the form against.
47      */
48     public Form(HttpServletRequest hsr) {
49         this(hsr, null);
50     }
51
52     /**
53      * Creates a new {@link Form}.
54      * 
55      * @param hsr
56      *            the request to register the form against.
57      * @param action
58      *            the target path where the form should be submitted.
59      */
60     public Form(HttpServletRequest hsr, String action) {
61         csrf = RandomToken.generateToken(32);
62         this.action = action;
63         HttpSession hs = hsr.getSession();
64         hs.setAttribute("form/" + getClass().getName() + "/" + csrf, this);
65     }
66
67     /**
68      * Update the forms internal state based on submitted data.
69      * 
70      * @param req
71      *            the request to take the initial data from.
72      * @return true, iff the form succeeded and the user should be redirected.
73      * @throws GigiApiException
74      *             if form data had problems or operations went wrong.
75      */
76     public abstract boolean submit(HttpServletRequest req) throws GigiApiException;
77
78     /**
79      * Calls {@link #submit(PrintWriter, HttpServletRequest)} while catching and
80      * displaying errors ({@link GigiApiException}), and re-outputing the form
81      * via {@link #output(PrintWriter, Language, Map)}.
82      * 
83      * @param out
84      *            the target to write the form and errors to
85      * @param req
86      *            the request that this submit originated (for submit and for
87      *            language)
88      * @return as {@link #submit(PrintWriter, HttpServletRequest)}: true, iff
89      *         the form succeeded and the user should be redirected.
90      */
91     public boolean submitProtected(PrintWriter out, HttpServletRequest req) {
92         try {
93             boolean succeeded = submit(req);
94             if (succeeded) {
95                 HttpSession hs = req.getSession();
96                 hs.removeAttribute("form/" + getClass().getName() + "/" + csrf);
97                 return true;
98             }
99         } catch (GigiApiException e) {
100             e.format(out, LoginPage.getLanguage(req));
101         }
102         output(out, LoginPage.getLanguage(req), new HashMap<String, Object>());
103         return false;
104     }
105
106     public boolean submitExceptionProtected(HttpServletRequest req) {
107         try {
108             if (submit(req)) {
109                 HttpSession hs = req.getSession();
110                 hs.removeAttribute("form/" + getClass().getName() + "/" + csrf);
111                 return true;
112             }
113             return false;
114         } catch (PermamentFormException e) {
115             req.setAttribute(SUBMIT_EXCEPTION, e);
116             return false;
117         } catch (GigiApiException e) {
118             req.setAttribute(SUBMIT_EXCEPTION, e);
119             return false;
120         }
121     }
122
123     /**
124      * Prints any errors in any form submits on this request.
125      * 
126      * @param req
127      *            The request to extract the errors from.
128      * @param out
129      *            the output stream to the user to write the errors to.
130      * @return true if no permanent errors occurred and the form should be
131      *         reprinted.
132      */
133     public static boolean printFormErrors(HttpServletRequest req, PrintWriter out) {
134         Object o = req.getAttribute(SUBMIT_EXCEPTION);
135         if (o != null && (o instanceof PermamentFormException)) {
136             ((PermamentFormException) o).getCause().format(out, Page.getLanguage(req));
137             return false;
138         }
139         if (o != null && (o instanceof GigiApiException)) {
140             ((GigiApiException) o).format(out, Page.getLanguage(req));
141         }
142         return true;
143     }
144
145     protected String getCsrfFieldName() {
146         return CSRF_FIELD;
147     }
148
149     @Override
150     public void output(PrintWriter out, Language l, Map<String, Object> vars) {
151         if (action == null) {
152             out.println("<form method='POST'>");
153         } else {
154             out.println("<form method='POST' action='" + action + "'>");
155         }
156         outputContent(out, l, vars);
157         out.print("<input type='hidden' name='" + CSRF_FIELD + "' value='");
158         out.print(getCSRFToken());
159         out.println("'></form>");
160     }
161
162     /**
163      * Outputs the forms contents.
164      * 
165      * @param out
166      *            Stream to the user.
167      * @param l
168      *            {@link Language} to translate text to.
169      * @param vars
170      *            Variables supplied from the outside.
171      */
172     protected abstract void outputContent(PrintWriter out, Language l, Map<String, Object> vars);
173
174     protected String getCSRFToken() {
175         return csrf;
176     }
177
178     /**
179      * Re-fetches a form e.g. when a Post-request is received.
180      * 
181      * @param req
182      *            the request that is directed to the form.
183      * @param target
184      *            the {@link Class} of the expected form.
185      * @return the form where this request is directed to.
186      * @throws CSRFException
187      *             if no CSRF-token is found or the token is wrong.
188      */
189     @SuppressWarnings("unchecked")
190     public static <T extends Form> T getForm(HttpServletRequest req, Class<T> target) throws CSRFException {
191         String csrf = req.getParameter(CSRF_FIELD);
192         if (csrf == null) {
193             throw new CSRFException();
194         }
195         HttpSession hs = req.getSession();
196         if (hs == null) {
197             throw new CSRFException();
198         }
199         Object f = hs.getAttribute("form/" + target.getName() + "/" + csrf);
200         if (f == null) {
201             throw new CSRFException();
202         }
203         if ( !(f instanceof Form)) {
204             throw new CSRFException();
205         }
206         if ( !target.isInstance(f)) {
207             throw new CSRFException();
208         }
209         // Dynamic Cast checked by previous if statement
210         return (T) f;
211     }
212
213     public static class CSRFException extends IOException {
214
215         private static final long serialVersionUID = 59708247477988362L;
216
217     }
218 }