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;
018
019import java.lang.reflect.Field;
020import java.lang.reflect.Modifier;
021import java.util.Collection;
022import java.util.TimeZone;
023
024import javax.servlet.http.Cookie;
025
026import org.apache.wicket.markup.html.pages.BrowserInfoPage;
027import org.apache.wicket.request.IRequestParameters;
028import org.apache.wicket.request.cycle.RequestCycle;
029import org.apache.wicket.request.http.WebRequest;
030import org.apache.wicket.util.io.IClusterable;
031import org.apache.wicket.util.string.AppendingStringBuffer;
032
033
034/**
035 * Description of various user agent (browser) properties. To fill the properties with values from
036 * the user agent you need to probe the browser using javascript and request header analysis. Wicket
037 * provides a default implementation of this in {@link BrowserInfoPage}.
038 * <p>
039 * A convenient way of letting Wicket do a sneaky redirect to {@link BrowserInfoPage} (and back
040 * again) is to put this in your Application's init method:
041 *
042 * <pre>
043 * getRequestCycleSettings().setGatherExtendedBrowserInfo(true);
044 * </pre>
045 *
046 * <p>
047 *
048 * WARNING: Be sure you think about the dangers of depending on information you pull from the client
049 * too much. They may be easily spoofed or inaccurate in other ways, and properties like window and
050 * browser size are all too easy to be used naively.
051 *
052 * @see BrowserInfoPage
053 * @author Frank Bille (frankbille)
054 */
055public class ClientProperties implements IClusterable
056{
057        private static final long serialVersionUID = 1L;
058
059        private int browserHeight = -1;
060        private int browserWidth = -1;
061        private boolean navigatorCookieEnabled;
062        private boolean navigatorJavaEnabled;
063        private String navigatorAppCodeName;
064        private String navigatorAppName;
065        private String navigatorAppVersion;
066        private String navigatorLanguage;
067        private String navigatorPlatform;
068        private String navigatorUserAgent;
069        private String remoteAddress;
070        private int screenColorDepth = -1;
071        private int screenHeight = -1;
072        private int screenWidth = -1;
073        private String utcDSTOffset;
074        private String utcOffset;
075        private String jsTimeZone;
076        private String hostname;
077
078        private boolean javaScriptEnabled;
079
080        /** Cached timezone for repeating calls to {@link #getTimeZone()} */
081        private transient TimeZone timeZone;
082
083        /**
084         * @return The browser height at the time it was measured
085         */
086        public int getBrowserHeight()
087        {
088                return browserHeight;
089        }
090
091        /**
092         * @return The browser width at the time it was measured
093         */
094        public int getBrowserWidth()
095        {
096                return browserWidth;
097        }
098
099        /**
100         * @return The client's navigator.appCodeName property.
101         */
102        public String getNavigatorAppCodeName()
103        {
104                return navigatorAppCodeName;
105        }
106
107        /**
108         * @return The client's navigator.appName property.
109         */
110        public String getNavigatorAppName()
111        {
112                return navigatorAppName;
113        }
114
115        /**
116         * @return The client's navigator.appVersion property.
117         */
118        public String getNavigatorAppVersion()
119        {
120                return navigatorAppVersion;
121        }
122
123        /**
124         * @return The client's navigator.language (or navigator.userLanguage) property.
125         */
126        public String getNavigatorLanguage()
127        {
128                return navigatorLanguage;
129        }
130
131        /**
132         * @return The client's navigator.platform property.
133         */
134        public String getNavigatorPlatform()
135        {
136                return navigatorPlatform;
137        }
138
139        /**
140         * @return The client's navigator.userAgent property.
141         */
142        public String getNavigatorUserAgent()
143        {
144                return navigatorUserAgent;
145        }
146
147        /**
148         * @return The client's remote/ip address.
149         */
150        public String getRemoteAddress()
151        {
152                return remoteAddress;
153        }
154
155        /**
156         * @return The clients hostname shown in the browser
157         */
158        public String getHostname()
159        {
160                return hostname;
161        }
162
163        /**
164         * @return Color depth of the screen in bits (integer).
165         */
166        public int getScreenColorDepth()
167        {
168                return screenColorDepth;
169        }
170
171        /**
172         * @return Height of the screen in pixels (integer).
173         */
174        public int getScreenHeight()
175        {
176                return screenHeight;
177        }
178
179        /**
180         * @return Height of the screen in pixels (integer).
181         */
182        public int getScreenWidth()
183        {
184                return screenWidth;
185        }
186
187        /**
188         * Get the client's time zone if that could be detected.
189         *
190         * @return The client's time zone
191         */
192        public TimeZone getTimeZone()
193        {
194                if (timeZone == null && jsTimeZone != null)
195                {
196                        TimeZone temptimeZone = TimeZone.getTimeZone(jsTimeZone);
197                        if (jsTimeZone.equals(temptimeZone.getID()))
198                        {
199                                timeZone = temptimeZone;
200                        }
201                }
202                if (timeZone == null)
203                {
204                        String utc = getUtcOffset();
205                        if (utc != null)
206                        {
207                                // apparently it is platform dependent on whether you get the
208                                // offset in a decimal form or not. This parses the decimal
209                                // form of the UTC offset, taking into account several
210                                // possibilities
211                                // such as getting the format in +2.5 or -1.2
212
213                                int dotPos = utc.indexOf('.');
214                                if (dotPos >= 0)
215                                {
216                                        String hours = utc.substring(0, dotPos);
217                                        String hourPart = utc.substring(dotPos + 1);
218
219                                        if (hours.startsWith("+"))
220                                        {
221                                                hours = hours.substring(1);
222                                        }
223                                        int offsetHours = Integer.parseInt(hours);
224                                        int offsetMins = (int)(Double.parseDouble(hourPart) * 6);
225
226                                        // construct a GMT timezone offset string from the retrieved
227                                        // offset which can be parsed by the TimeZone class.
228
229                                        AppendingStringBuffer sb = new AppendingStringBuffer("GMT");
230                                        sb.append(offsetHours > 0 ? '+' : '-');
231                                        sb.append(Math.abs(offsetHours));
232                                        sb.append(':');
233                                        if (offsetMins < 10)
234                                        {
235                                                sb.append('0');
236                                        }
237                                        sb.append(offsetMins);
238                                        timeZone = TimeZone.getTimeZone(sb.toString());
239                                }
240                                else
241                                {
242                                        int offset = Integer.parseInt(utc);
243                                        if (offset < 0)
244                                        {
245                                                utc = utc.substring(1);
246                                        }
247                                        timeZone = TimeZone.getTimeZone("GMT" + ((offset > 0) ? '+' : '-') + utc);
248                                }
249
250                                String dstOffset = getUtcDSTOffset();
251                                if (timeZone != null && dstOffset != null)
252                                {
253                                        TimeZone dstTimeZone;
254                                        dotPos = dstOffset.indexOf('.');
255                                        if (dotPos >= 0)
256                                        {
257                                                String hours = dstOffset.substring(0, dotPos);
258                                                String hourPart = dstOffset.substring(dotPos + 1);
259
260                                                if (hours.startsWith("+"))
261                                                {
262                                                        hours = hours.substring(1);
263                                                }
264                                                int offsetHours = Integer.parseInt(hours);
265                                                int offsetMins = (int)(Double.parseDouble(hourPart) * 6);
266
267                                                // construct a GMT timezone offset string from the
268                                                // retrieved
269                                                // offset which can be parsed by the TimeZone class.
270
271                                                AppendingStringBuffer sb = new AppendingStringBuffer("GMT");
272                                                sb.append(offsetHours > 0 ? '+' : '-');
273                                                sb.append(Math.abs(offsetHours));
274                                                sb.append(':');
275                                                if (offsetMins < 10)
276                                                {
277                                                        sb.append('0');
278                                                }
279                                                sb.append(offsetMins);
280                                                dstTimeZone = TimeZone.getTimeZone(sb.toString());
281                                        }
282                                        else
283                                        {
284                                                int offset = Integer.parseInt(dstOffset);
285                                                if (offset < 0)
286                                                {
287                                                        dstOffset = dstOffset.substring(1);
288                                                }
289                                                dstTimeZone = TimeZone.getTimeZone("GMT" + ((offset > 0) ? '+' : '-') +
290                                                        dstOffset);
291                                        }
292                                        // if the dstTimezone (1 July) has a different offset then
293                                        // the real time zone (1 January) try to combine the 2.
294                                        if (dstTimeZone != null &&
295                                                dstTimeZone.getRawOffset() != timeZone.getRawOffset())
296                                        {
297                                                int dstSaving = Math.abs(dstTimeZone.getRawOffset() - timeZone.getRawOffset());
298                                                String[] availableIDs = TimeZone.getAvailableIDs(dstTimeZone.getRawOffset() < timeZone.getRawOffset() ? dstTimeZone.getRawOffset() : timeZone.getRawOffset());
299                                                for (String availableID : availableIDs)
300                                                {
301                                                        TimeZone zone = TimeZone.getTimeZone(availableID);
302                                                        if (zone.getDSTSavings() == dstSaving)
303                                                        {
304                                                                // this is a best guess... still the start and end of the DST should
305                                                                // be needed to know to be completely correct, or better yet
306                                                                // not just the GMT offset but the TimeZone ID should be transfered
307                                                                // from the browser.
308                                                                timeZone = zone;
309                                                                break;
310                                                        }
311                                                }
312                                        }
313                                }
314                        }
315                }
316
317                return timeZone;
318        }
319
320        /**
321         * @return The client's time DST offset from UTC in hours (note: if you do this yourself, use
322         *         'new Date(new Date().getFullYear(), 0, 6, 0, 0, 0, 0).getTimezoneOffset() / -60'
323         *         (note the -)).
324         */
325        public String getUtcDSTOffset()
326        {
327                return utcDSTOffset;
328        }
329
330
331        /**
332         * @return The client's time offset from UTC in hours (note: if you do this yourself, use 'new
333         *         Date(new Date().getFullYear(), 0, 1, 0, 0, 0, 0).getTimezoneOffset() / -60' (note the
334         *         -)).
335         */
336        public String getUtcOffset()
337        {
338                return utcOffset;
339        }
340
341        /**
342         * Flag indicating support of JavaScript in the browser.
343         *
344         * @return True if JavaScript is enabled
345         */
346        public boolean isJavaScriptEnabled() {
347                return javaScriptEnabled;
348        }
349
350        /**
351         *
352         *
353         * @return The client's navigator.cookieEnabled property.
354         */
355        public boolean isNavigatorCookieEnabled()
356        {
357                if (!navigatorCookieEnabled && RequestCycle.get() != null)
358                {
359                        Collection<Cookie> cookies = ((WebRequest)RequestCycle.get().getRequest()).getCookies();
360                        navigatorCookieEnabled = cookies != null && cookies.size() > 0;
361                }
362                return navigatorCookieEnabled;
363        }
364
365        /**
366         * @return The client's navigator.javaEnabled property.
367         */
368        public boolean isNavigatorJavaEnabled()
369        {
370                return navigatorJavaEnabled;
371        }
372
373        /**
374         * @param browserHeight
375         *            The height of the browser
376         */
377        public void setBrowserHeight(int browserHeight)
378        {
379                this.browserHeight = browserHeight;
380        }
381
382        /**
383         * @param browserWidth
384         *            The browser width
385         */
386        public void setBrowserWidth(int browserWidth)
387        {
388                this.browserWidth = browserWidth;
389        }
390
391        /**
392         * @param cookiesEnabled
393         *            The client's navigator.cookieEnabled property.
394         */
395        public void setNavigatorCookieEnabled(boolean cookiesEnabled)
396        {
397                this.navigatorCookieEnabled = cookiesEnabled;
398        }
399
400        /**
401         * @param navigatorJavaEnabled
402         *            The client's navigator.javaEnabled property.
403         */
404        public void setNavigatorJavaEnabled(boolean navigatorJavaEnabled)
405        {
406                this.navigatorJavaEnabled = navigatorJavaEnabled;
407        }
408
409        /**
410         * @param navigatorAppCodeName
411         *            The client's navigator.appCodeName property.
412         */
413        public void setNavigatorAppCodeName(String navigatorAppCodeName)
414        {
415                this.navigatorAppCodeName = navigatorAppCodeName;
416        }
417
418        /**
419         * @param navigatorAppName
420         *            The client's navigator.appName property.
421         */
422        public void setNavigatorAppName(String navigatorAppName)
423        {
424                this.navigatorAppName = navigatorAppName;
425        }
426
427        /**
428         * @param navigatorAppVersion
429         *            The client's navigator.appVersion property.
430         */
431        public void setNavigatorAppVersion(String navigatorAppVersion)
432        {
433                this.navigatorAppVersion = navigatorAppVersion;
434        }
435
436        /**
437         * @param navigatorLanguage
438         *            The client's navigator.language (or navigator.userLanguage) property.
439         */
440        public void setNavigatorLanguage(String navigatorLanguage)
441        {
442                this.navigatorLanguage = navigatorLanguage;
443        }
444
445        /**
446         * @param navigatorPlatform
447         *            The client's navigator.platform property.
448         */
449        public void setNavigatorPlatform(String navigatorPlatform)
450        {
451                this.navigatorPlatform = navigatorPlatform;
452        }
453
454        /**
455         * @param navigatorUserAgent
456         *            The client's navigator.userAgent property.
457         */
458        public void setNavigatorUserAgent(String navigatorUserAgent)
459        {
460                this.navigatorUserAgent = navigatorUserAgent;
461        }
462
463        /**
464         * @param remoteAddress
465         *            The client's remote/ip address.
466         */
467        public void setRemoteAddress(String remoteAddress)
468        {
469                this.remoteAddress = remoteAddress;
470        }
471
472        /**
473         * @param hostname
474         *            the hostname shown in the browser.
475         */
476        public void setHostname(String hostname)
477        {
478                this.hostname = hostname;
479        }
480
481        /**
482         * @param screenColorDepth
483         *            Color depth of the screen in bits (integer).
484         */
485        public void setScreenColorDepth(int screenColorDepth)
486        {
487                this.screenColorDepth = screenColorDepth;
488        }
489
490        /**
491         * @param screenHeight
492         *            Height of the screen in pixels (integer).
493         */
494        public void setScreenHeight(int screenHeight)
495        {
496                this.screenHeight = screenHeight;
497        }
498
499        /**
500         * @param screenWidth
501         *            Height of the screen in pixels (integer).
502         */
503        public void setScreenWidth(int screenWidth)
504        {
505                this.screenWidth = screenWidth;
506        }
507
508        /**
509         * Sets time zone.
510         *
511         * @param timeZone
512         */
513        public void setTimeZone(TimeZone timeZone)
514        {
515                this.timeZone = timeZone;
516        }
517
518        /**
519         * @param utcDSTOffset
520         */
521        public void setUtcDSTOffset(String utcDSTOffset)
522        {
523                this.utcDSTOffset = utcDSTOffset;
524        }
525
526        /**
527         * @param utcOffset
528         *            The client's time offset from UTC in minutes (note: if you do this yourself, use
529         *            'new Date().getTimezoneOffset() / -60' (note the -)).
530         */
531        public void setUtcOffset(String utcOffset)
532        {
533                this.utcOffset = utcOffset;
534        }
535
536        /**
537         * @param jsTimeZone
538         */
539        public void setJsTimeZone(String jsTimeZone)
540        {
541                this.jsTimeZone = jsTimeZone;
542        }
543
544        /**
545         * @param javaScriptEnabled
546         *            is JavaScript supported in the browser
547         */
548        public void setJavaScriptEnabled(boolean javaScriptEnabled) {
549                this.javaScriptEnabled = javaScriptEnabled;
550        }
551
552        @Override
553        public String toString()
554        {
555                StringBuilder b = new StringBuilder();
556
557                Class<?> clazz = getClass();
558                while (clazz != Object.class) {
559                        Field[] fields = clazz.getDeclaredFields();
560
561                        for (Field field : fields)
562                        {
563                                // Ignore these fields
564                                if (Modifier.isStatic(field.getModifiers()) ||
565                                        Modifier.isTransient(field.getModifiers())  ||
566                                        field.isSynthetic())
567                                {
568                                        continue;
569                                }
570
571                                field.setAccessible(true);
572
573                                Object value;
574                                try
575                                {
576                                        value = field.get(this);
577                                }
578                                catch (IllegalArgumentException e)
579                                {
580                                        throw new RuntimeException(e);
581                                }
582                                catch (IllegalAccessException e)
583                                {
584                                        throw new RuntimeException(e);
585                                }
586
587                                if (field.getType().equals(Integer.TYPE))
588                                {
589                                        if (Integer.valueOf(-1).equals(value))
590                                        {
591                                                value = null;
592                                        }
593                                }
594
595                                if (value != null)
596                                {
597                                        b.append(field.getName());
598                                        b.append('=');
599                                        b.append(value);
600                                        b.append('\n');
601                                }
602                        }
603
604                        clazz = clazz.getSuperclass();
605                }
606                return b.toString();
607        }
608
609        /**
610         * Read parameters.
611         *
612         * @param parameters
613         *            parameters sent from browser
614         */
615        public void read(IRequestParameters parameters)
616        {
617                setNavigatorAppCodeName(parameters.getParameterValue("navigatorAppCodeName").toString("N/A"));
618                setNavigatorAppName(parameters.getParameterValue("navigatorAppName").toString("N/A"));
619                setNavigatorAppVersion(parameters.getParameterValue("navigatorAppVersion").toString("N/A"));
620                setNavigatorCookieEnabled(parameters.getParameterValue("navigatorCookieEnabled").toBoolean(false));
621                setNavigatorJavaEnabled(parameters.getParameterValue("navigatorJavaEnabled").toBoolean(false));
622                setNavigatorLanguage(parameters.getParameterValue("navigatorLanguage").toString("N/A"));
623                setNavigatorPlatform(parameters.getParameterValue("navigatorPlatform").toString("N/A"));
624                setNavigatorUserAgent(parameters.getParameterValue("navigatorUserAgent").toString("N/A"));
625                setScreenWidth(parameters.getParameterValue("screenWidth").toInt(-1));
626                setScreenHeight(parameters.getParameterValue("screenHeight").toInt(-1));
627                setScreenColorDepth(parameters.getParameterValue("screenColorDepth").toInt(-1));
628                setUtcOffset(parameters.getParameterValue("utcOffset").toString(null));
629                setUtcDSTOffset(parameters.getParameterValue("utcDSTOffset").toString(null));
630                setJsTimeZone(parameters.getParameterValue("jsTimeZone").toString(null));
631                setBrowserWidth(parameters.getParameterValue("browserWidth").toInt(-1));
632                setBrowserHeight(parameters.getParameterValue("browserHeight").toInt(-1));
633                setHostname(parameters.getParameterValue("hostname").toString("N/A"));
634        }
635}