]> WPIA git - gigi.git/blob - src/club/wpia/gigi/output/template/Form.java
upd: get default variables into outputables for error messages
[gigi.git] / src / club / wpia / gigi / output / template / Form.java
1 package club.wpia.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 club.wpia.gigi.GigiApiException;
13 import club.wpia.gigi.localisation.Language;
14 import club.wpia.gigi.pages.Page;
15 import club.wpia.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.endsForm()) {
140                 HttpSession hs = req.getSession();
141                 hs.removeAttribute("form/" + getClass().getName() + "/" + csrf);
142             }
143             if (res instanceof RedirectResult) {
144                 resp.sendRedirect(((RedirectResult) res).target);
145                 return true;
146             }
147             return false;
148         } catch (PermamentFormException e) {
149             req.setAttribute(SUBMIT_RESULT, e);
150             req.getSession().removeAttribute("form/" + getClass().getName() + "/" + csrf);
151             return false;
152         } catch (GigiApiException e) {
153             req.setAttribute(SUBMIT_RESULT, e);
154             return false;
155         }
156     }
157
158     /**
159      * Prints any errors in any form submits on this request.
160      * 
161      * @param req
162      *            The request to extract the errors from.
163      * @param out
164      *            the output stream to the user to write the errors to.
165      * @return true if no permanent errors occurred and the form should be
166      *         reprinted (and it has not already been successfully submitted)
167      */
168     public static boolean printFormErrors(HttpServletRequest req, PrintWriter out) {
169         Object o = req.getAttribute(SUBMIT_RESULT);
170         if (o != null && (o instanceof PermamentFormException)) {
171             ((PermamentFormException) o).getCause().format(out, Page.getLanguage(req), Page.getDefaultVars(req));
172             return false;
173         }
174         if (o != null && (o instanceof GigiApiException)) {
175             ((GigiApiException) o).format(out, Page.getLanguage(req), Page.getDefaultVars(req));
176             return true;
177         }
178         if (o != null && (o instanceof FormContinue)) {
179             return true;
180         }
181         if (o != null && (o instanceof SuccessMessageResult)) {
182             Outputable message = ((SuccessMessageResult) o).message;
183             if (message != null) {
184                 out.println("<div class='alert alert-success'>");
185                 message.output(out, Page.getLanguage(req), new HashMap<String, Object>());
186                 out.println("</div>");
187             }
188             return false;
189         }
190         return true;
191     }
192
193     protected String getCsrfFieldName() {
194         return CSRF_FIELD;
195     }
196
197     @Override
198     public void output(PrintWriter out, Language l, Map<String, Object> vars) {
199         if (action == null) {
200             out.println("<form method='POST'>");
201         } else {
202             out.println("<form method='POST' action='" + action + "'>");
203         }
204         outputContent(out, l, vars);
205         out.print("<input type='hidden' name='" + CSRF_FIELD + "' value='");
206         out.print(getCSRFToken());
207         out.println("'></form>");
208     }
209
210     /**
211      * Outputs the forms contents.
212      * 
213      * @param out
214      *            Stream to the user.
215      * @param l
216      *            {@link Language} to translate text to.
217      * @param vars
218      *            Variables supplied from the outside.
219      */
220     protected abstract void outputContent(PrintWriter out, Language l, Map<String, Object> vars);
221
222     protected String getCSRFToken() {
223         return csrf;
224     }
225
226     /**
227      * Re-fetches a form e.g. when a Post-request is received.
228      * 
229      * @param req
230      *            the request that is directed to the form.
231      * @param target
232      *            the {@link Class} of the expected form.
233      * @return the form where this request is directed to.
234      * @throws CSRFException
235      *             if no CSRF-token is found or the token is wrong.
236      */
237     @SuppressWarnings("unchecked")
238     public static <T extends Form> T getForm(HttpServletRequest req, Class<T> target) throws CSRFException {
239         String csrf = req.getParameter(CSRF_FIELD);
240         if (csrf == null) {
241             throw new CSRFException();
242         }
243         HttpSession hs = req.getSession();
244         if (hs == null) {
245             throw new CSRFException();
246         }
247         Object f = hs.getAttribute("form/" + target.getName() + "/" + csrf);
248         if (f == null) {
249             throw new CSRFException();
250         }
251         if ( !(f instanceof Form)) {
252             throw new CSRFException();
253         }
254         if ( !target.isInstance(f)) {
255             throw new CSRFException();
256         }
257         // Dynamic Cast checked by previous if statement
258         return (T) f;
259     }
260
261     public static class CSRFException extends IOException {
262
263         private static final long serialVersionUID = 59708247477988362L;
264
265     }
266 }