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.util.cookies;
018
019import java.time.Instant;
020import java.time.LocalDateTime;
021import java.time.ZoneId;
022
023import javax.servlet.http.Cookie;
024import org.apache.wicket.markup.html.form.FormComponent;
025import org.apache.wicket.protocol.http.WebApplication;
026import org.apache.wicket.protocol.http.servlet.ServletWebRequest;
027import org.apache.wicket.request.Response;
028import org.apache.wicket.request.cycle.RequestCycle;
029import org.apache.wicket.request.http.WebRequest;
030import org.apache.wicket.request.http.WebResponse;
031import org.apache.wicket.util.string.Strings;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035
036/**
037 * Helper class to simplify Cookie handling.
038 *
039 * @author Juergen Donnerstag
040 * @author Jonathan Locke
041 */
042public class CookieUtils
043{
044        private final static Logger log = LoggerFactory.getLogger(CookieUtils.class);
045
046        public static final String DEFAULT_SESSIONID_COOKIE_NAME = "JSESSIONID";
047
048        private final CookieDefaults settings;
049
050        /**
051         * Construct.
052         */
053        public CookieUtils()
054        {
055                settings = new CookieDefaults();
056        }
057
058        /**
059         * Construct.
060         *
061         * @param settings
062         *          the default settings for the saved cookies
063         */
064        public CookieUtils(final CookieDefaults settings)
065        {
066                this.settings = settings;
067        }
068
069        /**
070         * @return Gets the settings for these utils
071         */
072        public final CookieDefaults getSettings()
073        {
074                return settings;
075        }
076
077        /**
078         * Remove the cookie identified by the key
079         *
080         * @param key
081         *          The cookie name
082         */
083        public final void remove(final String key)
084        {
085                final Cookie cookie = getCookie(key);
086                if (cookie != null)
087                {
088                        remove(cookie);
089                }
090        }
091
092        /**
093         * Remove the cookie identified by the form component
094         *
095         * @param formComponent
096         */
097        public final void remove(final FormComponent<?> formComponent)
098        {
099                remove(getKey(formComponent));
100        }
101
102        /**
103         * This method gets used when a cookie key needs to be derived from a form component. By default
104         * the component's page relative path is used.
105         *
106         * @param component
107         * @return cookie key
108         */
109        protected String getKey(final FormComponent<?> component)
110        {
111                return getSaveKey(component.getPageRelativePath());
112        }
113
114        /**
115         * Retrieve the cookie value by means of its key.
116         *
117         * @param key
118         *          The cookie name
119         * @return The cookie value associated with the key
120         */
121        public final String load(final String key)
122        {
123                final Cookie cookie = getCookie(key);
124                if (cookie != null)
125                {
126                        return cookie.getValue();
127                }
128                return null;
129        }
130
131        /**
132         * Retrieve the cookie value associated with the formComponent and load the model object with
133         * the cookie value.
134         *
135         * @param formComponent
136         * @return The Cookie value which has also been used to set the component's model value
137         */
138        public final String load(final FormComponent<?> formComponent)
139        {
140                String value = load(getKey(formComponent));
141                if (value != null)
142                {
143                        // Assign the retrieved/persisted value to the component
144                        formComponent.setModelValue(new String[] {value});
145                }
146                return value;
147        }
148
149        /**
150         * Create a Cookie with key and value and save it in the browser with the next response
151         *
152         * @param name
153         *          The cookie name
154         * @param value
155         *          The cookie value
156         */
157        public final void save(String name, final String value)
158        {
159                Cookie cookie = getCookie(name);
160                if (cookie == null)
161                {
162                        cookie = new Cookie(name, value);
163                }
164                else
165                {
166                        cookie.setValue(value);
167                }
168                save(cookie);
169        }
170
171        /**
172         * Save the form components model value in a cookie
173         *
174         * @param formComponent
175         */
176        public final void save(final FormComponent<?> formComponent)
177        {
178                save(getKey(formComponent), formComponent.getValue());
179        }
180
181        /**
182         * Make sure the 'key' does not contain any illegal chars. E.g. for cookies ':' is not allowed.
183         *
184         * @param key
185         *            The key to be validated
186         * @return The save key
187         */
188        protected String getSaveKey(String key)
189        {
190                if (Strings.isEmpty(key))
191                {
192                        throw new IllegalArgumentException("A Cookie name can not be null or empty");
193                }
194
195                // cookie names cannot contain ':',
196                // we replace ':' with '.' but first we have to encode '.' as '..'
197                key = Strings.replaceAll(key, ".", "..").toString();
198                key = key.replace(':', '.');
199                return key;
200        }
201
202        /**
203         * Convenience method for deleting a cookie by name. Delete the cookie by setting its maximum
204         * age to zero.
205         *
206         * @param cookie
207         *            The cookie to delete
208         */
209        private void remove(final Cookie cookie)
210        {
211                if (cookie != null)
212                {
213                        save(cookie);
214
215                        // Delete the cookie by setting its maximum age to zero
216                        cookie.setMaxAge(0);
217                        cookie.setValue(null);
218
219                        if (log.isDebugEnabled())
220                        {
221                                log.debug("Removed Cookie: " + cookie.getName());
222                        }
223                }
224        }
225
226        /**
227         * Gets the cookie with 'name' attached to the latest WebRequest.
228         *
229         * @param name
230         *            The name of the cookie to be looked up
231         *
232         * @return Any cookies for this request
233         */
234        public Cookie getCookie(final String name)
235        {
236                try
237                {
238                        WebRequest webRequest = getWebRequest();
239                        Cookie cookie = webRequest.getCookie(name);
240                        if (log.isDebugEnabled())
241                        {
242                                if (cookie != null)
243                                {
244                                        log.debug("Found Cookie with name=" + name + " and request URI=" +
245                                                        webRequest.getUrl().toString());
246                                }
247                                else
248                                {
249                                        log.debug("Unable to find Cookie with name=" + name + " and request URI=" +
250                                                        webRequest.getUrl().toString());
251                                }
252                        }
253
254                        return cookie;
255                }
256                catch (NullPointerException ex)
257                {
258                        // Ignore any app server problem here
259                }
260
261                return null;
262        }
263
264
265        /**
266         * Gets the name of the cookie where the session id is stored.
267         *
268         * @param application
269         *            The current we application holding the {@link javax.servlet.ServletContext}.
270         *
271         * @return The name set in {@link javax.servlet.SessionCookieConfig} or the default value 'JSESSIONID' if not set
272         */
273        public String getSessionIdCookieName(WebApplication application)
274        {
275                String jsessionCookieName = application.getServletContext().getSessionCookieConfig().getName();
276
277                return jsessionCookieName == null ? DEFAULT_SESSIONID_COOKIE_NAME : jsessionCookieName;
278        }
279
280        /**
281         * Persist/save the data using Cookies.
282         *
283         * @param cookie
284         *            The Cookie to be persisted.
285         * @return The cookie provided
286         */
287        private Cookie save(final Cookie cookie)
288        {
289                if (cookie == null)
290                {
291                        return null;
292                }
293
294                initializeCookie(cookie);
295
296                getWebResponse().addCookie(cookie);
297
298                if (log.isDebugEnabled())
299                {
300                        log.debug("Cookie saved: " + cookieToDebugString(cookie) + "; request URI=" +
301                                getWebRequest().getUrl().toString());
302                }
303
304                return cookie;
305        }
306
307        /**
308         * Is called before the Cookie is saved. May be subclassed for different (dynamic) Cookie
309         * parameters. Static parameters can also be changed via {@link CookieDefaults}.
310         *
311         * @param cookie
312         */
313        protected void initializeCookie(final Cookie cookie)
314        {
315                final String comment = settings.getComment();
316                if (comment != null)
317                {
318                        cookie.setComment(comment);
319                }
320
321                final String domain = settings.getDomain();
322                if (domain != null)
323                {
324                        cookie.setDomain(domain);
325                }
326
327                ServletWebRequest request = (ServletWebRequest)getWebRequest();
328                String path = request.getContainerRequest().getContextPath() + "/" +
329                        request.getFilterPrefix();
330
331                cookie.setPath(path);
332                cookie.setVersion(settings.getVersion());
333                cookie.setSecure(settings.getSecure());
334                cookie.setMaxAge(settings.getMaxAge());
335                cookie.setHttpOnly(settings.isHttpOnly());
336        }
337
338        /**
339         * Convenience method to get the http request.
340         *
341         * @return WebRequest related to the RequestCycle
342         */
343        private WebRequest getWebRequest()
344        {
345                return (WebRequest)RequestCycle.get().getRequest();
346        }
347
348        /**
349         * Convenience method to get the http response.
350         *
351         * @return WebResponse related to the RequestCycle
352         */
353        private WebResponse getWebResponse()
354        {
355                RequestCycle cycle = RequestCycle.get();
356                Response response = cycle.getResponse();
357                if (!(response instanceof WebResponse))
358                {
359                        response = cycle.getOriginalResponse();
360                }
361                return (WebResponse)response;
362        }
363
364        /**
365         * Gets debug info as a string for the given cookie.
366         *
367         * @param cookie
368         *            the cookie to debug.
369         * @return a string that represents the internals of the cookie.
370         */
371        private String cookieToDebugString(final Cookie cookie)
372        {
373                final LocalDateTime localDateTime = Instant.ofEpochMilli(cookie.getMaxAge()).atZone(ZoneId.systemDefault()).toLocalDateTime();
374                return "[Cookie " + " name = " + cookie.getName() + ", value = " + cookie.getValue() +
375                        ", domain = " + cookie.getDomain() + ", path = " + cookie.getPath() + ", maxAge = " +
376                        localDateTime + "(" + cookie.getMaxAge() + ")" + "]";
377        }
378}