package er.extensions.appserver; import java.util.Enumeration; import org.apache.log4j.Logger; import sun.misc.BASE64Encoder; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WORequest; import com.webobjects.appserver._private.WOProperties; import com.webobjects.appserver._private.WOShared; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSComparator; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import er.extensions.foundation.ERXProperties; import er.extensions.localization.ERXLocalizer; /** Subclass of WORequest that fixes several Bugs. * The ID's are #2924761 and #2961017. It can also be extended to handle * #2957558 ("de-at" is converted to "German" instead of "German_Austria"). * The request is created via {@link ERXApplication#createRequest(String,String,String, NSDictionary,NSData,NSDictionary)}. */ public class ERXRequest extends WORequest { /** logging support */ public static final Logger log = Logger.getLogger(ERXRequest.class); public static final String UNKNOWN_HOST = "UNKNOWN"; protected static Boolean isBrowserFormValueEncodingOverrideEnabled; protected static final NSArray<String> HOST_ADDRESS_KEYS = new NSArray<String>(new String[]{"pc-remote-addr", "remote_host", "remote_addr", "remote_user", "x-webobjects-remote-addr"}); protected static final NSArray<String> HOST_NAME_KEYS = new NSArray<String>(new String[]{"x-forwarded-host", "Host", "x-webobjects-server-name", "server_name", "http_host"}); /** NSArray to keep browserLanguages in. */ protected NSArray<String> _browserLanguages; /** holds a reference to the browser object */ protected ERXBrowser _browser; /** * Specifies whether https should be overridden to be enabled or disabled app-wide. This is * useful if you are developing with DirectConnect and you want to be able to specif secure * forms and links, but you want to be able to continue testing them without setting up SSL. * * Defaults to false, set er.extensions.ERXRequest.secureDisabled=true to turn it off. */ protected boolean _secureDisabled; /** Simply call superclass constructor */ public ERXRequest(String string, String string0, String string1, NSDictionary nsdictionary, NSData nsdata, NSDictionary nsdictionary2) { super(string, string0, string1, nsdictionary, nsdata, nsdictionary2); if (isBrowserFormValueEncodingOverrideEnabled() && browser().formValueEncoding() != null) { setDefaultFormValueEncoding(browser().formValueEncoding()); } _secureDisabled = ERXRequest._isSecureDisabled(); } /** * Returns true if er.extensions.ERXRequest.secureDisabled is true. * * @return true if er.extensions.ERXRequest.secureDisabled is true */ public static boolean _isSecureDisabled() { return ERXProperties.booleanForKeyWithDefault("er.extensions.ERXRequest.secureDisabled", false); } /** * Returns true if er.extensions.ERXRequest.secureDisabled is true. * * @return true if er.extensions.ERXRequest.secureDisabled is true */ public boolean isSecureDisabled() { return _secureDisabled; } public boolean isBrowserFormValueEncodingOverrideEnabled() { if (isBrowserFormValueEncodingOverrideEnabled == null) { isBrowserFormValueEncodingOverrideEnabled = ERXProperties.booleanForKeyWithDefault("er.extensions.ERXRequest.BrowserFormValueEncodingOverrideEnabled", false) ? Boolean.TRUE : Boolean.FALSE; } return isBrowserFormValueEncodingOverrideEnabled.booleanValue(); } public WOContext context() { return _context(); } /** Returns a cooked version of the languages the user has set in his Browser. * Adds "Nonlocalized" and {@link ERXLocalizer#defaultLanguage()} if not * already present. Transforms regionalized en_us to English_US as a key. * @return cooked version of user's languages */ @Override @SuppressWarnings("unchecked") public NSArray<String> browserLanguages() { if (_browserLanguages == null) { NSMutableArray<String> languageKeys = new NSMutableArray<String>(); NSArray<String> fixedLanguages = null; String string = this.headerForKey("accept-language"); if (string != null) { NSArray<String> rawLanguages = NSArray.componentsSeparatedByString(string, ","); fixedLanguages = fixAbbreviationArray(rawLanguages); for (Enumeration<String> e = fixedLanguages.objectEnumerator(); e.hasMoreElements();) { String languageKey = e.nextElement(); String language = (String) WOProperties.TheLanguageDictionary.objectForKey(languageKey); if(language == null) { int index = languageKey.indexOf('_'); if(index > 0) { String mainLanguageKey = languageKey.substring(0, index); String region = languageKey.substring(index); language = (String) WOProperties.TheLanguageDictionary.objectForKey(mainLanguageKey); if(language != null) { language = language + region.toUpperCase(); } } } if(language != null) { languageKeys.addObject(language); } } } languageKeys.addObject("Nonlocalized"); if(!languageKeys.containsObject(ERXLocalizer.defaultLanguage())) { languageKeys.addObject(ERXLocalizer.defaultLanguage()); } _browserLanguages = languageKeys.immutableClone(); } return _browserLanguages; } @Override public String stringFormValueForKey(String key) { String result = super.stringFormValueForKey(key); if (result == null && "wodata".equals(key)) { // AK: yet another crappy 5.4 fix, WODynamicURL changed packages String requestHandlerKey = (String)valueForKeyPath("_uriDecomposed.requestHandlerKey"); if (WOApplication.application().resourceRequestHandlerKey().equals(requestHandlerKey)) { String requestHandlerPath = (String)valueForKeyPath("_uriDecomposed.requestHandlerPath"); requestHandlerPath = "file:/" + requestHandlerPath.substring("wodata=/".length()); // result = requestHandlerPath.replace('+', ' '); try { result = java.net.URLDecoder.decode( requestHandlerPath, "UTF-8"); } catch (java.io.UnsupportedEncodingException e) { log.error("unable to decode wodata parameter", e); result = requestHandlerPath.replace('+', ' '); } } } return result; } /** * Gets the ERXBrowser associated with the user-agent of * the request. * @return browser object for the request */ public ERXBrowser browser() { if (_browser == null) { ERXBrowserFactory browserFactory = ERXBrowserFactory.factory(); _browser = browserFactory.browserMatchingRequest(this); browserFactory.retainBrowser(_browser); } return _browser; } /** * Cleaning up retian count on the browser. */ @Override public void finalize() throws Throwable { if (_browser != null) { ERXBrowserFactory.factory().releaseBrowser(_browser); } super.finalize(); } /** * Returns whether or not this request is secure. * * @return whether or not this request is secure */ public boolean isSecure() { return ERXRequest.isRequestSecure(this); } @Override public void _completeURLPrefix(StringBuffer stringbuffer, boolean secure, int port) { if (_secureDisabled) { secure = false; } String serverName = _serverName(); String portStr; if (port == 0) { portStr = secure ? "443" : _serverPort(); } else { portStr = WOShared.unsignedIntString(port); } if (secure) { stringbuffer.append("https://"); } else { stringbuffer.append("http://"); } stringbuffer.append(serverName); if(portStr != null && ((secure && !"443".equals(portStr)) || (!secure && !"80".equals(portStr)))) { stringbuffer.append(':'); stringbuffer = stringbuffer.append(portStr); } } /** * Returns whether or not the given request is secure. * MS: I found this somewhere else a while ago, but I have no idea where or * I'd give attribution. * * @param request the request to check * @return whether or not the given request is secure. */ public static boolean isRequestSecure(WORequest request) { boolean isRequestSecure = false; // Depending on the adaptor the incoming port can be found in one of two // places. if (request != null) { String serverPort = request.headerForKey("SERVER_PORT"); if (serverPort == null) { serverPort = request.headerForKey("x-webobjects-server-port"); } // Apache and some other web servers use this to indicate HTTPS mode. String httpsMode = request.headerForKey("https"); // If either the https header is 'on' or the server port is 443 then we // consider this to be an HTTP request. isRequestSecure = ((httpsMode != null && httpsMode.equalsIgnoreCase("on")) || (serverPort != null && "443".equals(serverPort))); } return isRequestSecure; } private static class _LanguageComparator extends NSComparator { private static float quality(String languageString) { float result=0f; if (languageString!=null) { languageString = languageString.trim(); int semicolon=languageString.indexOf(';'); if (semicolon!=-1 && languageString.length()>semicolon+2) { result=Float.parseFloat(languageString.substring(semicolon+1).trim().substring(2)); } else result=1.0f; } return result; } @Override public int compare(Object o1, Object o2) { float f1=quality((String)o1); float f2=quality((String)o2); return f1<f2 ? OrderedDescending : ( f1==f2 ? OrderedSame : OrderedAscending ); // we want DESCENDING SORT!! } } /** Translates ("de", "en-us;q=0.33", "en", "en-gb;q=0.66") to ("de", "en_gb", "en-us", "en"). * @param languages NSArray of Strings * @return sorted NSArray of normalized Strings */ private final static NSComparator COMPARE_Qs = new _LanguageComparator(); protected NSArray<String> fixAbbreviationArray(NSArray<String> languages) { try { languages=languages.sortedArrayUsingComparator(COMPARE_Qs); } catch (NSComparator.ComparisonException e) { log.warn("Couldn't sort language array "+languages+": "+e); } catch (NumberFormatException e2) { log.warn("Couldn't sort language array "+languages+": "+e2); } NSMutableArray<String> languagePrefix = new NSMutableArray<String>(languages.count()); for (int languageNum = languages.count() - 1; languageNum >= 0; languageNum--) { String language = languages.objectAtIndex(languageNum); int offset; language = language.trim(); offset = language.indexOf(';'); if (offset > 0) { language = language.substring(0, offset); } offset = language.indexOf('-'); if (offset > 0) { String langPrefix = language.substring(0, offset); // "en" part of "en-us" if (!languagePrefix.containsObject(langPrefix)) { languagePrefix.insertObjectAtIndex(langPrefix, 0); } // converts "en-us" into "en_us"; String cooked = language.replace('-', '_'); language = cooked; } languagePrefix.insertObjectAtIndex(language, 0); } return languagePrefix; } /** * Overridden because malformed cookie to return an empty dictionary * if the super implementation throws an exception. This will happen * if the request contains malformed cookie values. */ @Override public NSDictionary cookieValues() { try { return super.cookieValues(); } catch (Throwable t) { log.warn(t + ":" + this); log.warn(t); return NSDictionary.EmptyDictionary; } } /** * Overridden because the super implementation would pull in all * content even if the request is supposed to be streaming and thus * very large. Will now return <code>false</code> if the request * handler is streaming. */ @Override public boolean isSessionIDInRequest() { ERXApplication app = (ERXApplication)WOApplication.application(); if (app.isStreamingRequestHandlerKey(requestHandlerKey())) { return false; } else { return super.isSessionIDInRequest(); } } /** * Overridden because the super implementation would pull in all * content even if the request is supposed to be streaming and thus * very large. Will now look for the session ID only in the cookie * values. */ @Override protected String _getSessionIDFromValuesOrCookie(boolean inCookiesFirst) { ERXApplication app = (ERXApplication)WOApplication.application(); boolean wis = WOApplication.application().streamActionRequestHandlerKey().equals(requestHandlerKey()); boolean alternateStreaming = app.isStreamingRequestHandlerKey(requestHandlerKey()); boolean streaming = wis || alternateStreaming; String sessionID = null; if(inCookiesFirst) { sessionID = cookieValueForKey("wosid"); if(sessionID == null && !streaming) { sessionID = stringFormValueForKey("wosid"); } } else { if(!streaming) { sessionID = stringFormValueForKey("wosid"); } if(sessionID == null) { sessionID = cookieValueForKey("wosid"); } } return sessionID; } /** * Utility method to set credentials for basic authorization. * */ public void setCredentials(String userName, String password) { String up = userName + ":" + password; BASE64Encoder coder = new BASE64Encoder(); byte[] bytes = up.getBytes(); String encodedString = coder.encode(bytes); setHeader("Basic " + encodedString, "authorization"); } /** * @deprecated Use remoteHostAddress() instead */ @Deprecated public String remoteHost() { return remoteHostAddress(); } /** * Returns the remote client host address. Works in various setups, like * direct connect, deployed etc. If no host name can be found, * returns "UNKNOWN". * * @return remote client host address */ public String remoteHostAddress() { if (WOApplication.application().isDirectConnectEnabled()) { if (_originatingAddress() != null) { return _originatingAddress().getHostAddress(); } } for (String key : HOST_ADDRESS_KEYS) { String remoteAddressHeaderValue = headerForKey(key); if (remoteAddressHeaderValue != null) { return remoteAddressHeaderValue; } } return UNKNOWN_HOST; } /** * Returns the remote client host name. If no host name can be found, * returns "UNKNOWN". * * @return remote client host name */ public String remoteHostName() { for (String key : HOST_NAME_KEYS) { if (headerForKey(key) != null) { return headerForKey(key); } } return UNKNOWN_HOST; } public NSMutableDictionary mutableUserInfo() { NSDictionary userInfo = userInfo(); NSMutableDictionary mutableUserInfo; if (userInfo == null) { mutableUserInfo = new NSMutableDictionary(); setUserInfo(mutableUserInfo); } else if (userInfo instanceof NSMutableDictionary) { mutableUserInfo = (NSMutableDictionary) userInfo; } else { mutableUserInfo = userInfo.mutableClone(); setUserInfo(mutableUserInfo); } return mutableUserInfo; } }