001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.protocol.http.servlet;
018
019import java.nio.charset.Charset;
020import java.time.Instant;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collections;
024import java.util.Enumeration;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029import java.util.Set;
030import javax.servlet.http.Cookie;
031import javax.servlet.http.HttpServletRequest;
032import javax.servlet.http.HttpServletResponse;
033import org.apache.commons.fileupload.FileItemFactory;
034import org.apache.commons.fileupload.FileUploadException;
035import org.apache.wicket.protocol.http.RequestUtils;
036import org.apache.wicket.request.IRequestParameters;
037import org.apache.wicket.request.IWritableRequestParameters;
038import org.apache.wicket.request.Url;
039import org.apache.wicket.request.UrlUtils;
040import org.apache.wicket.request.http.WebRequest;
041import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException;
042import org.apache.wicket.util.lang.Args;
043import org.apache.wicket.util.lang.Bytes;
044import org.apache.wicket.util.string.PrependingStringBuffer;
045import org.apache.wicket.util.string.StringValue;
046import org.apache.wicket.util.string.Strings;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050/**
051 * {@link WebRequest} subclass that wraps a {@link HttpServletRequest} object.
052 * 
053 * @author Matej Knopp
054 * @author Juergen Donnerstag
055 * @author Igor Vaynberg
056 */
057public class ServletWebRequest extends WebRequest
058{
059        private static final Logger LOG = LoggerFactory.getLogger(ServletWebRequest.class);
060
061        private final HttpServletRequest httpServletRequest;
062
063        private final Url url;
064
065        private final String filterPrefix;
066
067        private final ErrorAttributes errorAttributes;
068
069        private final ForwardAttributes forwardAttributes;
070
071        /**
072         * Construct.
073         * 
074         * @param httpServletRequest
075         * @param filterPrefix
076         *            contentPath + filterPath, used to extract the actual {@link Url}
077         */
078        public ServletWebRequest(HttpServletRequest httpServletRequest, String filterPrefix)
079        {
080                this(httpServletRequest, filterPrefix, null);
081        }
082
083        /**
084         * Construct.
085         * 
086         * @param httpServletRequest
087         * @param filterPrefix
088         *            contentPath + filterPath, used to extract the actual {@link Url}
089         * @param url
090         */
091        public ServletWebRequest(HttpServletRequest httpServletRequest, String filterPrefix, Url url)
092        {
093                Args.notNull(httpServletRequest, "httpServletRequest");
094                Args.notNull(filterPrefix, "filterPrefix");
095
096                this.httpServletRequest = httpServletRequest;
097
098                errorAttributes = ErrorAttributes.of(httpServletRequest, filterPrefix);
099
100                forwardAttributes = ForwardAttributes.of(httpServletRequest, filterPrefix);
101
102                this.filterPrefix = filterPrefix;
103
104                if (url != null)
105                {
106                        this.url = url;
107                }
108                else
109                {
110                        this.url = getContextRelativeUrl(httpServletRequest.getRequestURI(), filterPrefix);
111                }
112        }
113
114        /**
115         * Returns base url without context or filter mapping.
116         * <p>
117         * Example: if current url is
118         * 
119         * <pre>
120         * http://localhost:8080/context/filter/mapping/wicket/bookmarkable/com.foo.Page?1&amp;id=2
121         * </pre>
122         * 
123         * the base url is <em>wicket/bookmarkable/com.foo.Page</em>
124         * </p>
125         * 
126         * @see org.apache.wicket.request.Request#getClientUrl()
127         */
128        @Override
129        public Url getClientUrl()
130        {
131                if (errorAttributes != null && !Strings.isEmpty(errorAttributes.getRequestUri()))
132                {
133                        String problematicURI = Url.parse(errorAttributes.getRequestUri(), getCharset(), false)
134                                .toString();
135                        return getContextRelativeUrl(problematicURI, filterPrefix);
136                }
137                else if (forwardAttributes != null && !Strings.isEmpty(forwardAttributes.getRequestUri()))
138                {
139                        String forwardURI = Url.parse(forwardAttributes.getRequestUri(), getCharset(), false)
140                                .toString();
141                        return getContextRelativeUrl(forwardURI, filterPrefix);
142                }
143                else if (!isAjax())
144                {
145                        return getContextRelativeUrl(httpServletRequest.getRequestURI(), filterPrefix);
146                }
147                else
148                {
149                        String base = getHeader(HEADER_AJAX_BASE_URL);
150
151                        if (base == null)
152                        {
153                                base = getRequestParameters().getParameterValue(PARAM_AJAX_BASE_URL).toString(null);
154                        }
155
156                        if (base == null)
157                        {
158                                throw new AbortWithHttpErrorCodeException(HttpServletResponse.SC_BAD_REQUEST,
159                                        "Current ajax request is missing the base url header or parameter");
160                        }
161
162                        return setParameters(Url.parse(base, getCharset()));
163                }
164        }
165
166        private Url setParameters(Url url)
167        {
168                url.setPort(httpServletRequest.getServerPort());
169                url.setHost(httpServletRequest.getServerName());
170                url.setProtocol(httpServletRequest.getScheme());
171                url.setContextRelative(true);
172                return url;
173        }
174
175        private Url getContextRelativeUrl(String uri, String filterPrefix)
176        {
177                if (filterPrefix.length() > 0 && !filterPrefix.endsWith("/"))
178                {
179                        filterPrefix += "/";
180                }
181                StringBuilder url = new StringBuilder();
182                uri = Strings.stripJSessionId(uri);
183                String contextPath = httpServletRequest.getContextPath();
184
185                if (LOG.isDebugEnabled())
186                {
187                        LOG.debug("Calculating context relative path from: context path '{}', filterPrefix '{}', uri '{}'",
188                                        new Object[] {contextPath, filterPrefix, uri});
189                }
190
191                final int start = contextPath.length() + filterPrefix.length() + 1;
192                if (uri.length() > start)
193                {
194                        url.append(uri.substring(start));
195                }
196
197                if (errorAttributes == null)
198                {
199                        String query = httpServletRequest.getQueryString();
200                        if (!Strings.isEmpty(query))
201                        {
202                                url.append('?');
203                                url.append(query);
204                        }
205                }
206
207                return setParameters(Url.parse(url.toString(), getCharset(), false));
208        }
209
210        /**
211         * Returns the prefix of Wicket filter (without the leading /)
212         * 
213         * @return Wicket filter prefix
214         */
215        public String getFilterPrefix()
216        {
217                return filterPrefix;
218        }
219
220        @Override
221        public List<Cookie> getCookies()
222        {
223                Cookie[] cookies = httpServletRequest.getCookies();
224                List<Cookie> result = (cookies == null) ? Collections.<Cookie> emptyList()
225                        : Arrays.asList(cookies);
226                return Collections.unmodifiableList(result);
227        }
228
229
230        @Override
231        public Locale getLocale()
232        {
233                return httpServletRequest.getLocale();
234        }
235
236        @Override
237        public Instant getDateHeader(String name)
238        {
239                try
240                {
241                        long value = httpServletRequest.getDateHeader(name);
242
243                        if (value == -1)
244                        {
245                                return null;
246                        }
247
248                        return Instant.ofEpochMilli(value);
249                }
250                catch (IllegalArgumentException e)
251                {
252                        // per spec thrown if the header contains a value that cannot be converted to a date
253                        return null;
254                }
255        }
256
257        @Override
258        public String getHeader(String name)
259        {
260                return httpServletRequest.getHeader(name);
261        }
262
263        @SuppressWarnings("unchecked")
264        @Override
265        public List<String> getHeaders(String name)
266        {
267                List<String> result = new ArrayList<>();
268                Enumeration<String> e = httpServletRequest.getHeaders(name);
269                while (e.hasMoreElements())
270                {
271                        result.add(e.nextElement());
272                }
273                return Collections.unmodifiableList(result);
274        }
275
276        private Map<String, List<StringValue>> postParameters = null;
277
278        protected Map<String, List<StringValue>> generatePostParameters()
279        {
280                Map<String, List<StringValue>> postParameters = new HashMap<>();
281
282                IRequestParameters queryParams = getQueryParameters();
283
284                @SuppressWarnings("unchecked")
285                Map<String, String[]> params = getContainerRequest().getParameterMap();
286                for (Map.Entry<String, String[]> param : params.entrySet())
287                {
288                        final String name = param.getKey();
289                        final String[] values = param.getValue();
290
291                        if (name != null && values != null)
292                        {
293                                // build a mutable list of query params that have the same name as the post param
294                                List<StringValue> queryValues = queryParams.getParameterValues(name);
295                                if (queryValues == null)
296                                {
297                                        queryValues = Collections.emptyList();
298                                }
299                                else
300                                {
301                                        queryValues = new ArrayList<>(queryValues);
302                                }
303
304                                // the list that will contain accepted post param values
305                                List<StringValue> postValues = new ArrayList<>();
306
307                                for (String value : values)
308                                {
309                                        StringValue val = StringValue.valueOf(value);
310                                        if (queryValues.contains(val))
311                                        {
312                                                // if a query param with this value exists remove it and continue
313                                                queryValues.remove(val);
314                                        }
315                                        else
316                                        {
317                                                // there is no query param with this value, assume post
318                                                postValues.add(val);
319                                        }
320                                }
321
322                                if (!postValues.isEmpty())
323                                {
324                                        postParameters.put(name, postValues);
325                                }
326                        }
327                }
328                return postParameters;
329        }
330
331        private Map<String, List<StringValue>> getPostRequestParameters()
332        {
333                if (postParameters == null)
334                {
335                        postParameters = generatePostParameters();
336                }
337                return postParameters;
338        }
339
340        private final IRequestParameters postRequestParameters = new IWritableRequestParameters()
341        {
342                @Override
343                public void reset()
344                {
345                        getPostRequestParameters().clear();
346                }
347
348                @Override
349                public void setParameterValues(String key, List<StringValue> values)
350                {
351                        getPostRequestParameters().put(key, values);
352                }
353
354                @Override
355                public Set<String> getParameterNames()
356                {
357                        return Collections.unmodifiableSet(getPostRequestParameters().keySet());
358                }
359
360                @Override
361                public StringValue getParameterValue(String name)
362                {
363                        List<StringValue> values = getPostRequestParameters().get(name);
364                        if (values == null || values.isEmpty())
365                        {
366                                return StringValue.valueOf((String)null);
367                        }
368                        else
369                        {
370                                return values.iterator().next();
371                        }
372                }
373
374                @Override
375                public List<StringValue> getParameterValues(String name)
376                {
377                        List<StringValue> values = getPostRequestParameters().get(name);
378                        if (values != null)
379                        {
380                                values = Collections.unmodifiableList(values);
381                        }
382                        return values;
383                }
384        };
385
386        @Override
387        public IRequestParameters getPostParameters()
388        {
389                return postRequestParameters;
390        }
391
392        @Override
393        public Url getUrl()
394        {
395                return new Url(url);
396        }
397
398        @Override
399        public ServletWebRequest cloneWithUrl(Url url)
400        {
401                return new ServletWebRequest(httpServletRequest, filterPrefix, url)
402                {
403                        @Override
404                        public Url getOriginalUrl()
405                        {
406                            return ServletWebRequest.this.getOriginalUrl();
407                        }
408
409                        @Override
410                        public IRequestParameters getPostParameters()
411                        {
412                                // don't parse post parameters again
413                                return ServletWebRequest.this.getPostParameters();
414                        }
415                };
416        }
417
418        /**
419         * Creates multipart web request from this request.
420         * 
421         * @param maxSize
422         *            max allowed size of request
423         * @param upload
424         *            upload identifier for {@link UploadInfo}
425         * @return multipart request
426         * @throws FileUploadException
427         */
428        public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload)
429                throws FileUploadException
430        {
431                return new MultipartServletWebRequestImpl(getContainerRequest(), filterPrefix, maxSize, upload);
432        }
433
434        /**
435         * Creates multipart web request from this request.
436         *
437         * @param maxSize
438         *            max allowed size of request
439         * @param upload
440         *            upload identifier for {@link UploadInfo}
441         * @param factory
442         * @return multipart request
443         * @throws FileUploadException
444         */
445        public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload,
446                FileItemFactory factory) throws FileUploadException
447        {
448                return new MultipartServletWebRequestImpl(getContainerRequest(), filterPrefix, maxSize, upload, factory);
449        }
450
451        @Override
452        public String getPrefixToContextPath()
453        {
454                PrependingStringBuffer buffer = new PrependingStringBuffer();
455                Url filterPrefixUrl = Url.parse(filterPrefix, getCharset());
456                for (int i = 0; i < filterPrefixUrl.getSegments().size() - 1; ++i)
457                {
458                        buffer.prepend("../");
459                }
460                return buffer.toString();
461        }
462
463        @Override
464        public Charset getCharset()
465        {
466                return RequestUtils.getCharset(httpServletRequest);
467        }
468
469        @Override
470        public HttpServletRequest getContainerRequest()
471        {
472                return httpServletRequest;
473        }
474
475        @Override
476        public String getContextPath()
477        {
478                return UrlUtils.normalizePath(httpServletRequest.getContextPath());
479        }
480
481        @Override
482        public String getFilterPath()
483        {
484                return UrlUtils.normalizePath(filterPrefix);
485        }
486
487        @Override
488        public boolean shouldPreserveClientUrl()
489        {
490                return (errorAttributes != null && !Strings.isEmpty(errorAttributes.getRequestUri()) || forwardAttributes != null &&
491                        !Strings.isEmpty(forwardAttributes.getRequestUri()));
492        }
493}