/* * Copyright (C) NetStruxr, Inc. All rights reserved. * * This software is published under the terms of the NetStruxr * Public Software License version 0.5, a copy of which has been * included with this distribution in the LICENSE.NPL file. */ package er.extensions.appserver; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.TimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WOCookie; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOResponse; import com.webobjects.appserver.WOSession; import com.webobjects.appserver.WOCookie.SameSite; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSKeyValueCodingAdditions; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSNotification; import com.webobjects.foundation.NSNotificationCenter; import com.webobjects.foundation.NSPathUtilities; import com.webobjects.foundation.NSSelector; import com.webobjects.foundation.NSTimestamp; import er.extensions.appserver.ajax.ERXAjaxSession; import er.extensions.eof.ERXConstant; import er.extensions.eof.ERXEC; import er.extensions.foundation.ERXArrayUtilities; import er.extensions.foundation.ERXProperties; import er.extensions.foundation.ERXStringUtilities; import er.extensions.foundation.ERXThreadStorage; import er.extensions.foundation.ERXValueUtilities; import er.extensions.localization.ERXLocalizer; /** * The ERXSession arguments the regular WOSession object * by adding a few nice additions. Of interest, notifications * are now posted when a session when a session * goes to sleep, David Neumann's browser backtracking detection * has been added from his security framework, a somewhat * comprehensive user-agent parsing is provided to know what type * of browser is being used, flags have also been added to tell * if javascript has been enabled, and enhanced localization * support has been added. */ public class ERXSession extends ERXAjaxSession implements Serializable { /** * Do I need to update serialVersionUID? * See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the * <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a> */ private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(ERXSession.class); /** * Notification name that is posted when a session is about to sleep. */ public static final String SessionWillSleepNotification = "SessionWillSleepNotification"; /** cookie name that if set it means that the user has cookies enabled */ // FIXME: This should be configurable public static final String JAVASCRIPT_ENABLED_COOKIE_NAME = "js"; /** the SameSite to set for session and instance cookies */ private static SameSite _sameSite = ERXProperties.enumValueForKey(SameSite.class, "er.extensions.ERXSession.cookies.SameSite"); /** holds a reference to the current localizer used for this session */ transient private ERXLocalizer _localizer; /** * special variable to hold language name only for when * session object gets serialized. * Do not use this value to get the language name; * use {@link #language} method instead. */ private String _serializableLanguageName; /** holds a reference to the current message encoding used for this session */ private ERXMessageEncoding _messageEncoding; /** holds a reference to the current browser used for this session */ transient private ERXBrowser _browser; /** flag for if java script is enabled */ protected Boolean _javaScriptEnabled; // most people have JS by now /** holds a debugging store for a given session. */ protected NSMutableDictionary _debuggingStore; /** the receiver of the various notifications */ transient private Observer _observer; /** the default session timeZone */ private TimeZone _timeZone = TimeZone.getDefault(); /** * _originalThreadName holds the original name from the WorkerThread which * is the value before executing <code>awake()</code> */ public String _originalThreadName; public ERXSession() { super(); } public ERXSession(String sessionID) { super(sessionID); } /** * returns the observer object for this session. * If it doesn't ever exist, one will be created. * * @return the observer */ public Observer observer() { if (_observer == null) _observer = new Observer(this); return _observer; } /** * The Observer inner class encapsulates functions * to handle various notifications. */ public static class Observer { /** the parent session */ transient protected ERXSession session; /** private constructor; prevents instantiation in this way */ private Observer() { super(); } /** creates observer object which works with the given session */ public Observer(ERXSession session) { super(); this.session = session; } /** * resets the reference to localizer when localization * templates or localizer class itself is updated. */ public void localizationDidReset(NSNotification n) { if (session._localizer == null) return; String currentLanguage = session._localizer.language(); session._localizer = ERXLocalizer.localizerForLanguage(currentLanguage); log.debug("Detected changes in the localizers. Reset reference to {} localizer for session {}", currentLanguage, session.sessionID()); } /** * registers this observer object for * {@link er.extensions.localization.ERXLocalizer#LocalizationDidResetNotification} */ protected void registerForLocalizationDidResetNotification() { NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("localizationDidReset", ERXConstant.NotificationClassArray), ERXLocalizer.LocalizationDidResetNotification, null); } } /** * Method to get the current localizer for this * session. If local instance variable is null * then a localizer is fetched for the session's * <code>languages</code> array. See {@link er.extensions.localization.ERXLocalizer} * for more information about using a localizer. * @return the current localizer for this session */ public ERXLocalizer localizer() { if (_localizer == null) { _localizer = ERXLocalizer.localizerForLanguages(languages()); if (!WOApplication.application().isCachingEnabled()) observer().registerForLocalizationDidResetNotification(); } return _localizer; } /** * Returns the primary language of the current * session's localizer. This method is just a * cover for calling the method * <code>localizer().language()</code>. * @return primary language */ public String language() { return localizer().language(); } /** * Cover method to set the current localizer * to the localizer for that language. * <p> * Also updates languages list with the new single language. * * @param language to set the current localizer for. * @see #language * @see #setLanguages */ public void setLanguage(String language) { ERXLocalizer newLocalizer = ERXLocalizer.localizerForLanguage(language); if (!newLocalizer.equals(_localizer)) { if (_localizer == null && !WOApplication.application().isCachingEnabled()) observer().registerForLocalizationDidResetNotification(); _localizer = newLocalizer; ERXLocalizer.setCurrentLocalizer(_localizer); if (browser() != null) { _messageEncoding = browser().messageEncodingForLanguage(_localizer.language()); } NSMutableArray languageList = new NSMutableArray(_localizer.language()); if (!languageList.containsObject("Nonlocalized")) languageList.addObject("Nonlocalized"); setLanguages(languageList); } } /** * Sets the languages list for which the session is localized. * The ordering of language strings in the array determines * the order in which the application will search .lproj * directories for localized strings, images, and component * definitions. * <p> * Also updates localizer and messageEncodings. * * @param languageList the array of languages for the session * @see #language * @see #setLanguage */ @Override public void setLanguages(NSArray languageList) { super.setLanguages(languageList); ERXLocalizer newLocalizer = ERXLocalizer.localizerForLanguages(languageList); if (!newLocalizer.equals(_localizer)) { if (_localizer == null && !WOApplication.application().isCachingEnabled()) observer().registerForLocalizationDidResetNotification(); _localizer = newLocalizer; ERXLocalizer.setCurrentLocalizer(_localizer); if (browser() != null) { _messageEncoding = browser().messageEncodingForLanguage(_localizer.language()); } } } /** * Returns the NSArray of language names available for * this application. This is simply a cover method of * {@link er.extensions.localization.ERXLocalizer#availableLanguages}, * but will be convenient for binding to dynamic elements * like language selector popup. * * @return NSArray of language name strings available * for this application * @see #availableLanguagesForThisSession * @see er.extensions.localization.ERXLocalizer#availableLanguages */ public NSArray availableLanguagesForTheApplication() { return ERXLocalizer.availableLanguages(); } /** * Returns the NSArray of language names available for * this particular session. * The resulting array is an intersect of web browser's * language array ({@link ERXRequest#browserLanguages()}) * and localizer's available language array * ({@link er.extensions.localization.ERXLocalizer#availableLanguages()}). * <p> * Note that the order of the resulting language names * is not defined at this moment. * * @return NSArray of language name strings available * for this particular session * @see #availableLanguagesForTheApplication * @see ERXRequest#browserLanguages() * @see er.extensions.localization.ERXLocalizer#availableLanguages */ public NSArray availableLanguagesForThisSession() { NSArray browserLanguages = null; if (context() != null && context().request() != null) browserLanguages = context().request().browserLanguages(); return ERXArrayUtilities.intersectingElements(browserLanguages, ERXLocalizer.availableLanguages()); } /** * Returns the message encoding of the current session. * If it's not already set up but no current <code>language()</code> * available for the session, it creates one with * the default encoding. * @return message encoding object */ public ERXMessageEncoding messageEncoding() { if (_messageEncoding == null) { if (browser() != null) { _messageEncoding = browser().messageEncodingForLanguage(language()); } } return _messageEncoding; } /** * Returns the browser object representing the web * browser's "user-agent" string. You can obtain * browser name, version, platform and Mozilla version, etc. * through this object. <br> * Good for WOConditional's condition binding to deal * with different browser versions. * @return browser object */ public ERXBrowser browser() { if (_browser == null && context() != null) { WORequest request = context().request(); if (request != null) { ERXBrowserFactory browserFactory = ERXBrowserFactory.factory(); if (request instanceof ERXRequest) { _browser = ((ERXRequest) request).browser(); } else { _browser = browserFactory.browserMatchingRequest(request); } browserFactory.retainBrowser(_browser); } } return _browser; } /** * Simple mutable dictionary that can be used at * runtime to stash objects that can be useful for * debugging. * @return debugging store dictionary */ // ENHANCEME: Should perform a check to make sure that the app is not in production mode when this is being used. public NSMutableDictionary debuggingStore() { if (_debuggingStore == null) _debuggingStore = new NSMutableDictionary(); return _debuggingStore; } private boolean _editingContextWasCreated = false; /** * Ensures that the returned editingContext was created with * the {@link er.extensions.eof.ERXEC} factory. * @return the session's default editing context with * the default delegate set. */ @Override public EOEditingContext defaultEditingContext() { if (!_editingContextWasCreated) { setDefaultEditingContext(newDefaultEditingContext()); _editingContextWasCreated = true; } return super.defaultEditingContext(); } @Override public void setDefaultEditingContext(EOEditingContext ec) { _editingContextWasCreated = true; super.setDefaultEditingContext(ec); } /** * Returns if this user has javascript enabled. * This checks a form value "javaScript" and a cookie "js" * if the value is 1. * @return if js is enabled, defaults to true. */ //CHECKME: (ak) I don't understand this? Having a default of TRUE makes no sense. At least not without some ERXJSCheckJavaScript component that would set a value on <NOSCRIPT> or sth like this. public boolean javaScriptEnabled() { WORequest request = context() != null ? context().request() : null; if (_javaScriptEnabled == null && request != null) { // FIXME: Shouldn't be hardcoded form value. String js = request.stringFormValueForKey("javaScript"); if (js != null) { log.debug("Received javascript form value {}", js); } else { try { js = request.cookieValueForKey(JAVASCRIPT_ENABLED_COOKIE_NAME); } catch (StringIndexOutOfBoundsException e) { // malformed cookies cause WO 5.1.3 to raise here } } if (js != null) { _javaScriptEnabled = (ERXValueUtilities.booleanValue(js) && (browser().browserName().equals(ERXBrowser.UNKNOWN_BROWSER) || browser().isMozilla40Compatible() || browser().isMozilla50Compatible())) ? Boolean.TRUE : Boolean.FALSE; } } // defaults to true as most browsers have javascript return _javaScriptEnabled == null ? true : _javaScriptEnabled.booleanValue(); } /** * Sets if javascript is enabled for this session. * crafty entry pages can set form values via * javascript to test if it is enabled. * @param newValue says if javascript is enabled */ public void setJavaScriptEnabled(boolean newValue) { _javaScriptEnabled = newValue ? Boolean.TRUE : Boolean.FALSE; } /** * Overridden to provide a few checks to * see if javascript is enabled. */ @Override public void awake() { super.awake(); ERXSession.setSession(this); ERXLocalizer.setCurrentLocalizer(localizer()); NSNotificationCenter.defaultCenter().postNotification(SessionDidRestoreNotification, this); WORequest request = context() != null ? context().request() : null; if (request != null && log.isDebugEnabled() && request.headerForKey("content-type") != null) { if ((request.headerForKey("content-type")).toLowerCase().indexOf("multipart/form-data") == -1) log.debug("Form values {}", request.formValues()); else log.debug("Multipart Form values found"); } _originalThreadName = Thread.currentThread().getName(); Thread.currentThread().setName(threadName()); } /** * Overridden to post the notification that * the session will sleep. */ @Override public void sleep() { NSNotificationCenter.defaultCenter().postNotification(SessionWillSleepNotification, this); super.sleep(); ERXLocalizer.setCurrentLocalizer(null); ERXSession.setSession(null); // reset backtracking _didBacktrack = null; Thread.currentThread().setName(_originalThreadName); removeObjectForKey("ERXActionLogging"); } /** * Override this method in order to provide a different name for the WorkerThread for this * request-response loop very useful for logging stuff: assign a log statement to a log entry. * Something useful could be: * * <blockquote><code>return session().sessionID() + valueForKeyPath("user.username");</code></blockquote> * * @return name of the current thread */ public String threadName() { return Thread.currentThread().getName(); } /* * Backtrack detection - Pulled from David Neumann's wonderful security framework. */ /** * flag to indicate if the user is currently backtracking, * meaning they hit the back button and then clicked on a * link. */ protected Boolean _didBacktrack = null; /** flag to indicate if the last action was a direct action */ public boolean lastActionWasDA = false; /** * Utility method that gets the context ID string * from the passed in request. * @param aRequest request to get the context id from * @return the context id as a string */ // MOVEME: ERXWOUtilities public String requestsContextID(WORequest aRequest) { String uri = aRequest.uri(); int idx = uri.indexOf('?'); if (idx != -1) uri = uri.substring(0, idx); String eID = NSPathUtilities.lastPathComponent(uri); NSArray eIDs = NSArray.componentsSeparatedByString(eID, "."); String reqCID = "1"; if (eIDs.count() > 0) { reqCID = (String) eIDs.objectAtIndex(0); } return reqCID; } /** * Method inspects the passed in request to see if * the user backtracked. If the context ID for the request is 2 clicks * less than the context ID for the current WOContext, we know * the backtracked. * @return if the user has backtracked or not. */ public boolean didBacktrack() { if (_didBacktrack == null) { _didBacktrack = Boolean.FALSE; //If the current request is a direct action, no way the user could have backtracked. if (!context().request().requestHandlerKey().equals(WOApplication.application().directActionRequestHandlerKey())) { int reqCID = Integer.parseInt(requestsContextID(context().request())); int cid = Integer.parseInt(context().contextID()); int delta = cid - reqCID; if (delta > 2) { _didBacktrack = Boolean.TRUE; } else if (delta > 1) { // Might not have backtracked if their last // action was a direct action. // ERXDirectActionRequestHandler, which is the framework // built-in default direct action handler, sets this variable // to true at the end of its handleRequest method. if (!lastActionWasDA) { _didBacktrack = Boolean.TRUE; } } } lastActionWasDA = false; } return _didBacktrack.booleanValue(); } /** * Provides automatic encoding support for component action * with <code>messageEncoding</code> object. * @param aRequest current request * @param aContext current context */ @Override public void takeValuesFromRequest(WORequest aRequest, WOContext aContext) { messageEncoding().setDefaultFormValueEncodingToRequest(aRequest); super.takeValuesFromRequest(aRequest, aContext); } /** * Provides automatic encoding support for component action * with <code>messageEncoding</code> object. * @param aResponse current response object * @param aContext current context object */ @Override public void appendToResponse(WOResponse aResponse, WOContext aContext) { messageEncoding().setEncodingToResponse(aResponse); super.appendToResponse(aResponse, aContext); } /** * Bringing application into KVC. * * @return the application object */ public ERXApplication application() { return ERXApplication.erxApplication(); } /** * Overrides terminate to free up resources and unregister for notifications. */ @Override public void terminate() { if (_observer != null) { NSNotificationCenter.defaultCenter().removeObserver(_observer); _observer = null; } if (_browser != null) { ERXBrowserFactory.factory().releaseBrowser(_browser); _browser = null; } log.debug("Will terminate, sessionId is {}", sessionID()); super.terminate(); } private transient NSKeyValueCodingAdditions _objectStore; /** This is a cover method which enables use of the session's object store * which is usually access with setObjectForKey and objectForKey. One can use * this method with KVC, like for example in .wod bindings: * * <code> * myString: WOString { * value = session.objectStore.myLastSearchResult.count; * } * </code> * * @return an Object which implements KVC + KVC additions */ public NSKeyValueCodingAdditions objectStore() { if (_objectStore == null) { _objectStore = new NSKeyValueCodingAdditions() { public void takeValueForKey(Object arg0, String arg1) { if (arg0 == null) { removeObjectForKey(arg1); } else { setObjectForKey(arg0, arg1); } } public Object valueForKey(String arg0) { return objectForKey(arg0); } public void takeValueForKeyPath(Object arg0, String arg1) { if (arg0 == null) { removeObjectForKey(arg1); } else { setObjectForKey(arg0, arg1); } } public Object valueForKeyPath(String arg0) { Object theObject = objectForKey(arg0); if (theObject == null && arg0.indexOf(".") > -1) { String key = ""; String oriKey = arg0; do { key = key + oriKey.substring(0, oriKey.indexOf(".")); oriKey = oriKey.substring(oriKey.indexOf(".") + 1); theObject = objectForKey(key); key += "."; } while (theObject == null && oriKey.indexOf(".") > -1); if (theObject != null && !ERXStringUtilities.stringIsNullOrEmpty(oriKey)) { theObject = NSKeyValueCodingAdditions.Utility.valueForKeyPath(theObject, oriKey); } } return theObject; } }; } return _objectStore; } /* * Serialization support - enables to use a variety of session stores */ private void writeObject(ObjectOutputStream stream) throws IOException { if (_localizer == null) _serializableLanguageName = null; else _serializableLanguageName = language(); stream.defaultWriteObject(); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); if (_serializableLanguageName != null) setLanguage(_serializableLanguageName); log.debug("Session has been deserialized: {}", this); } @Override public NSTimestamp _birthDate() { return super._birthDate(); } @Override public String toString() { String superString = super.toString(); String thisString = " localizer=" + (_localizer == null ? "null" : _localizer.toString()) + " messageEncoding=" + (_messageEncoding == null ? "null" : _messageEncoding.toString()) + " browser=" + (_browser == null ? "null" : _browser.toString()); int lastIndex = superString.lastIndexOf(">"); String toStr; if (lastIndex > 0) { // ignores if ">" is the first char (lastIndex == 0) toStr = superString.substring(0, lastIndex - 1) + thisString + ">"; } else { toStr = superString + thisString; } return toStr; } @Override public EOEditingContext newDefaultEditingContext() { return ERXEC.newEditingContext(); } public static WOSession anySession() { return (WOSession) ERXThreadStorage.valueForKey("session"); } public static ERXSession session() { return (ERXSession) ERXThreadStorage.valueForKey("session"); } public static String currentSessionID() { return (String) ERXThreadStorage.valueForKey("ERXSession.sessionID"); } public static void setSession(ERXSession session) { ERXThreadStorage.takeValueForKey(session, "session"); ERXThreadStorage.takeValueForKey(session == null ? null : session.sessionID(), "ERXSession.sessionID"); } /** * Override and return true, or set er.extensions.ERXSession.useSecureSessionCookies if you want * secure-only session and instance cookies. This prevents cookie hijacking man-in-the-middle * attacks. If the cookies aren't set as secure only and an HTTP request is made, the cookies * will be sent over HTTP. So if someone manages to do an HTTP injection that causes an HTTP * request to be made, they can compromise your session id. For example, if you have a CMS on * https://www.mycms.com and you set a session id, and I hack in and trick your site and manage to * do an injection where i do an <img src="http://www.mycms.com/whatever"/> in the content, like I post * in a comment and you don't strip out HTML tags. secure-only just gives you peace-of-mind. If you * intended the cookies to only be behind HTTPS, secure-only makes it actually true and enforced. * * Note that to make this effective (and for sessions to work at all), your site must be behind HTTPS at all times. * In development mode, you can disable secure mode (@see er.extensions.ERXRequest.isSecureDisabled) for running in * direct-connect with this mode enabled. * * @return whether or not secure cookies are enabled */ public boolean useSecureSessionCookies() { return ERXProperties.booleanForKeyWithDefault("er.extensions.ERXSession.useSecureSessionCookies", false); } /** * Override and return true, or set er.extensions.ERXSession.useHttpOnlySessionCookies if you want http-only session * and instance cookies. This prevents the XSS attack. Note that after setting this true, you will not allowed to * read this cookies from yours javascript code. * * @return whether or not http-only cookies are enabled */ public static boolean useHttpOnlySessionCookies() { return ERXProperties.booleanForKeyWithDefault("er.extensions.ERXSession.useHttpOnlySessionCookies", false); } protected void _setCookieSameSite(WOResponse response) { if (storesIDsInCookies() && _sameSite != null) { for (WOCookie cookie : response.cookies()) { String sessionIdKey = application().sessionIdKey(); String instanceIdKey = application().instanceIdKey(); String cookieName = cookie.name(); if (sessionIdKey.equals(cookieName) || instanceIdKey.equals(cookieName)) { cookie.setSameSite(_sameSite); } } } } protected void _convertSessionCookiesToSecure(WOResponse response) { if (storesIDsInCookies() && !ERXRequest._isSecureDisabled()) { for (WOCookie cookie : response.cookies()) { String sessionIdKey = application().sessionIdKey(); String instanceIdKey = application().instanceIdKey(); String cookieName = cookie.name(); if (sessionIdKey.equals(cookieName) || instanceIdKey.equals(cookieName)) { cookie.setIsSecure(true); } } } } protected void _convertSessionCookiesToHttpOnly(final WOResponse response) { if (storesIDsInCookies()) { for (WOCookie cookie : response.cookies()) { String sessionIdKey = application().sessionIdKey(); String instanceIdKey = application().instanceIdKey(); String cookieName = cookie.name(); if (sessionIdKey.equals(cookieName) || instanceIdKey.equals(cookieName)) { cookie.setIsHttpOnly(true); } } } } @Override public void _appendCookieToResponse(WOResponse response) { super._appendCookieToResponse(response); if (useSecureSessionCookies()) { _convertSessionCookiesToSecure(response); } if (useHttpOnlySessionCookies()) { _convertSessionCookiesToHttpOnly(response); } _setCookieSameSite(response); } @Override public void _clearCookieFromResponse(WOResponse response) { super._clearCookieFromResponse(response); if (useSecureSessionCookies()) { _convertSessionCookiesToSecure(response); } if (useHttpOnlySessionCookies()) { _convertSessionCookiesToHttpOnly(response); } } public TimeZone timeZone() { return _timeZone; } public void setTimeZone(TimeZone timeZone) { if(timeZone == null) { throw new NullPointerException("timeZone must not be set to null"); } _timeZone = timeZone; } private static Boolean autoAdjustTimeZone; /** * If the property er.extensions.ERXSession.autoAdjustTimeZone=true then * WOStrings and WOTextFields with dateFormat bindings will be have their * formatter automatically adjusted for the selected session time zone. * @return value of er.extensions.ERXSession.autoAdjustTimeZone property. Default is false. */ public static boolean autoAdjustTimeZone() { if(autoAdjustTimeZone == null) { autoAdjustTimeZone = ERXProperties.booleanForKeyWithDefault("er.extensions.ERXSession.autoAdjustTimeZone", false); } return autoAdjustTimeZone; } //******************************************************************** // Current D2W Look for this Session // Because it is possible to have different Looks depends on the User //******************************************************************** /** * set the current Look for this Session * * <pre> * 90 : *true* => look = "session.currentD2WLook" * [er.directtoweb.assignments.delayed.ERDDelayedKeyValueAssignment] * </pre> * * @param currentD2WLook - Look Name */ public void setCurrentD2WLook(String currentD2WLook) { this.currentD2WLook = currentD2WLook; } /** * Get the current Look for this Session. * * @return Look Name */ public String currentD2WLook() { if(ERXStringUtilities.stringIsNullOrEmpty(currentD2WLook)) { currentD2WLook = "ERModernLook"; } return currentD2WLook; } private String currentD2WLook = null; }