]> WPIA git - gigi.git/blob - src/org/cacert/gigi/output/template/Form.java
fix: ResultSet.getDate is often wrong as it fetches day-precision times
[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.HttpServletResponse;
10 import javax.servlet.http.HttpSession;
11
12 import org.cacert.gigi.GigiApiException;
13 import org.cacert.gigi.localisation.Language;
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     /**
35      * Encapsulates a (non-failure) outcome of a form.
36      */
37     public static abstract class SubmissionResult {
38
39         public abstract boolean endsForm();
40     }
41
42     /**
43      * The form has finished and the user should see the successful completion
44      * on a regular page.
45      */
46     public static class RedirectResult extends SubmissionResult {
47
48         private final String target;
49
50         public RedirectResult(String target) {
51             this.target = target;
52         }
53
54         @Override
55         public boolean endsForm() {
56             return true;
57         }
58
59     }
60
61     /**
62      * The form has not finished and should be re-emitted, however no error
63      * occurred.
64      */
65     public static class FormContinue extends SubmissionResult {
66
67         @Override
68         public boolean endsForm() {
69             return false;
70         }
71     }
72
73     /**
74      * The form has successfully finished and a message should be emitted on a
75      * stateful page.
76      */
77     public static class SuccessMessageResult extends SubmissionResult {
78
79         private final Outputable message;
80
81         public SuccessMessageResult(Outputable message) {
82             this.message = message;
83         }
84
85         @Override
86         public boolean endsForm() {
87             return true;
88         }
89     }
90
91     public static final String CSRF_FIELD = "csrf";
92
93     public static final String SUBMIT_RESULT = "form-submit-result";
94
95     private final String csrf;
96
97     private final String action;
98
99     /**
100      * Creates a new {@link Form}.
101      * 
102      * @param hsr
103      *            the request to register the form against.
104      */
105     public Form(HttpServletRequest hsr) {
106         this(hsr, null);
107     }
108
109     /**
110      * Creates a new {@link Form}.
111      * 
112      * @param hsr
113      *            the request to register the form against.
114      * @param action
115      *            the target path where the form should be submitted.
116      */
117     public Form(HttpServletRequest hsr, String action) {
118         csrf = RandomToken.generateToken(32);
119         this.action = action;
120         HttpSession hs = hsr.getSession();
121         hs.setAttribute("form/" + getClass().getName() + "/" + csrf, this);
122     }
123
124     /**
125      * Update the forms internal state based on submitted data.
126      * 
127      * @param req
128      *            the request to take the initial data from.
129      * @return true, iff the form succeeded and the user should be redirected.
130      * @throws GigiApiException
131      *             if form data had problems or operations went wrong.
132      */
133     public abstract SubmissionResult submit(HttpServletRequest req) throws GigiApiException;
134
135     public boolean submitExceptionProtected(HttpServletRequest req, HttpServletResponse resp) throws IOException {
136         try {
137             SubmissionResult res = submit(req);
138             req.setAttribute(SUBMIT_RESULT, res);
139             if (res instanceof RedirectResult) {
140                 resp.sendRedirect(((RedirectResult) res).target);
141                 return true;
142             }
143             if (res.endsForm()) {
144                 HttpSession hs = req.getSession();
145                 hs.removeAttribute("form/" + getClass().getName() + "/" + csrf);
146             }
147             return false;
148         } catch (PermamentFormException e) {
149             req.setAttribute(SUBMIT_RESULT, e);
150             return false;
151         } catch (GigiApiException e) {
152             req.setAttribute(SUBMIT_RESULT, e);
153             return false;
154         }
155     }
156
157     /**
158      * Prints any errors in any form submits on this request.
159      * 
160      * @param req
161      *            The request to extract the errors from.
162      * @param out
163      *            the output stream to the user to write the errors to.
164      * @return true if no permanent errors occurred and the form should be
165      *         reprinted (and it has not already been successfully submitted)
166      */
167     public static boolean printFormErrors(HttpServletRequest req, PrintWriter out) {
168         Object o = req.getAttribute(SUBMIT_RESULT);
169         if (o != null && (o instanceof PermamentFormException)) {
170             ((PermamentFormException) o).getCause().format(out, Page.getLanguage(req));
171             return false;
172         }
173         if (o != null && (o instanceof GigiApiException)) {
174             ((GigiApiException) o).format(out, Page.getLanguage(req));
175             return true;
176         }
177         if (o != null && (o instanceof FormContinue)) {
178             return true;
179         }
180         if (o != null && (o instanceof SuccessMessageResult)) {
181             Outputable message = ((SuccessMessageResult) o).message;
182             if (message != null) {
183                 out.println("<div class='alert alert-success'>");
184                 message.output(out, Page.getLanguage(req), new HashMap<String, Object>());
185                 out.println("</div>");
186             }
187             return false;
188         }
189         return true;
190     }
191
192     protected String getCsrfFieldName() {
193         return CSRF_FIELD;
194     }
195
196     @Override
197     public void output(PrintWriter out, Language l, Map<String, Object> vars) {
198         if (action == null) {
199             out.println("<form method='POST'>");
200         } else {
201             out.println("<form method='POST' action='" + action + "'>");
202         }
203         outputContent(out, l, vars);
204         out.print("<input type='hidden' name='" + CSRF_FIELD + "' value='");
205         out.print(getCSRFToken());
206         out.println("'></form>");
207     }
208
209     /**
210      * Outputs the forms contents.
211      * 
212      * @param out
213      *            Stream to the user.
214      * @param l
215      *            {@link Language} to translate text to.
216      * @param vars
217      *            Variables supplied from the outside.
218      */
219     protected abstract void outputContent(PrintWriter out, Language l, Map<String, Object> vars);
220
221     protected String getCSRFToken() {
222         return csrf;
223     }
224
225     /**
226      * Re-fetches a form e.g. when a Post-request is received.
227      * 
228      * @param req
229      *            the request that is directed to the form.
230      * @param target
231      *            the {@link Class} of the expected form.
232      * @return the form where this request is directed to.
233      * @throws CSRFException
234      *             if no CSRF-token is found or the token is wrong.
235      */
236     @SuppressWarnings("unchecked")
237     public static <T extends Form> T getForm(HttpServletRequest req, Class<T> target) throws CSRFException {
238         String csrf = req.getParameter(CSRF_FIELD);
239         if (csrf == null) {
240             throw new CSRFException();
241         }
242         HttpSession hs = req.getSession();
243         if (hs == null) {
244             throw new CSRFException();
245         }
246         Object f = hs.getAttribute("form/" + target.getName() + "/" + csrf);
247         if (f == null) {
248             throw new CSRFException();
249         }
250         if ( !(f instanceof Form)) {
251             throw new CSRFException();
252         }
253         if ( !target.isInstance(f)) {
254             throw new CSRFException();
255         }
256         // Dynamic Cast checked by previous if statement
257         return (T) f;
258     }
259
260     public static class CSRFException extends IOException {
261
262         private static final long serialVersionUID = 59708247477988362L;
263
264     }
265 }