package er.extensions.appserver; import java.net.MalformedURLException; import java.net.URL; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WODirectAction; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOSession; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSNotification; import com.webobjects.foundation.NSNotificationCenter; import er.extensions.appserver.ajax.ERXAjaxContext; import er.extensions.foundation.ERXMutableURL; import er.extensions.foundation.ERXMutableUserInfoHolderInterface; import er.extensions.foundation.ERXProperties; import er.extensions.foundation.ERXRuntimeUtilities; import er.extensions.foundation.ERXSelectorUtilities; import er.extensions.foundation.ERXStringUtilities; import er.extensions.foundation.ERXThreadStorage; /** * Replacement of WOContext. This subclass is installed when the frameworks * loads. */ public class ERXWOContext extends ERXAjaxContext implements ERXMutableUserInfoHolderInterface { private static Observer observer; private boolean _generateCompleteURLs; private boolean _generateCompleteResourceURLs; private static final boolean IS_DEV = ERXApplication.isDevelopmentModeSafe(); public static final String CONTEXT_KEY = "wocontext"; public static final String CONTEXT_DICTIONARY_KEY = "ERXWOContext.dict"; public static class Observer { public void applicationDidHandleRequest(NSNotification n) { ERXWOContext.setCurrentContext(null); ERXThreadStorage.removeValueForKey(ERXWOContext.CONTEXT_DICTIONARY_KEY); } } /** * Returns the existing session if any is given in the form values or URL. * * @return session for this request or <code>null</code> */ public WOSession existingSession() { String sessionID = _requestSessionID(); if (!super.hasSession() && sessionID != null) WOApplication.application().restoreSessionWithID(sessionID, this); return _session(); } /** * Returns true if there is an existing session. */ @Override public boolean hasSession() { if (super.hasSession()) { return true; } return existingSession() != null; } public static NSMutableDictionary contextDictionary() { if (observer == null) { synchronized (ERXWOContext.class) { if (observer == null) { observer = new Observer(); NSNotificationCenter.defaultCenter().addObserver(observer, ERXSelectorUtilities.notificationSelector("applicationDidHandleRequest"), WOApplication.ApplicationDidDispatchRequestNotification, null); } } } NSMutableDictionary contextDictionary = ERXWOContext._contextDictionary(); if (contextDictionary == null) { contextDictionary = new NSMutableDictionary(); ERXThreadStorage.takeValueForKey(contextDictionary, ERXWOContext.CONTEXT_DICTIONARY_KEY); } return contextDictionary; } public static WOContext currentContext() { return (WOContext) ERXThreadStorage.valueForKey(CONTEXT_KEY); } public static void setCurrentContext(Object object) { ERXThreadStorage.takeValueForKey(object, CONTEXT_KEY); } protected static NSMutableDictionary _contextDictionary() { NSMutableDictionary contextDictionary = (NSMutableDictionary) ERXThreadStorage.valueForKey(ERXWOContext.CONTEXT_DICTIONARY_KEY); return contextDictionary; } public ERXWOContext(WORequest worequest) { super(worequest); } /** * Implemented so that the thread checks if it should get interrupted. * * @param component the current component */ @Override public void _setCurrentComponent(WOComponent component) { ERXRuntimeUtilities.checkThreadInterrupt(); super._setCurrentComponent(component); } @Override public Object clone() { ERXWOContext context = (ERXWOContext)super.clone(); context._setGenerateCompleteResourceURLs(_generateCompleteResourceURLs); return context; } /** * Turn on complete resource URL generation. * * @param generateCompleteResourceURLs if true, resources will generate complete URLs. */ public void _setGenerateCompleteResourceURLs(boolean generateCompleteResourceURLs) { _generateCompleteResourceURLs = generateCompleteResourceURLs; } /** * Returns whether or not resources generate complete URLs. * * @return whether or not resources generate complete URLs */ public boolean _generatingCompleteResourceURLs() { return _generateCompleteResourceURLs; } @Override public void generateCompleteURLs() { super.generateCompleteURLs(); _generateCompleteURLs = true; } @Override public void generateRelativeURLs() { super.generateRelativeURLs(); _generateCompleteURLs = false; } @Override public boolean doesGenerateCompleteURLs() { return _generateCompleteURLs; } /** * Creates a WOContext using a dummy WORequest. * @return the new WOContext */ public static WOContext newContext() { WOApplication app = WOApplication.application(); // Try to create a URL with a relative path into the application to mimic a real request. // We must create a request with a relative URL, as using an absolute URL makes the new // WOContext's URL absolute, and it is then unable to render relative paths. (Long story short.) // // Note: If you configured the adaptor's WebObjectsAlias to something other than the default, // make sure to also set your WOAdaptorURL property to match. Otherwise, asking the new context // the path to a direct action or component action URL will give an incorrect result. String requestUrl = app.cgiAdaptorURL() + "/" + app.name() + app.applicationExtension(); try { URL url = new URL(requestUrl); requestUrl = url.getPath(); // Get just the part of the URL that is relative to the server root. } catch (MalformedURLException mue) { // The above should never fail. As a last resort, using the empty string will // look funny in the request, but still allow the context to use a relative url. requestUrl = ""; } return app.createContextForRequest(app.createRequest("GET", requestUrl, "HTTP/1.1", null, null, null)); } public NSMutableDictionary mutableUserInfo() { return contextDictionary(); } public void setMutableUserInfo(NSMutableDictionary userInfo) { ERXThreadStorage.takeValueForKey(userInfo, ERXWOContext.CONTEXT_DICTIONARY_KEY); } @Override public NSDictionary userInfo() { return mutableUserInfo(); } /** * If er.extensions.ERXWOContext.forceRemoveApplicationNumber is true, then always remove the * application number from the generated URLs. You have to be aware of how your app is written * to know if this is something you can do without causing problems. For instance, you MUST be * using cookies, and you must not use WOImages with data bindings -- anything that requires a * per-instance cache has the potential to fail when this is enabled (if you have more than * one instance of your app deployed). */ protected void _preprocessURL() { if (ERXProperties.booleanForKey("er.extensions.ERXWOContext.forceRemoveApplicationNumber")) { _url().setApplicationNumber(null); } } protected String _postprocessURL(String url) { if (WOApplication.application() instanceof ERXApplication) { return ERXApplication.erxApplication()._rewriteURL(url); } return url; } @Override public String _urlWithRequestHandlerKey(String requestHandlerKey, String requestHandlerPath, String queryString, boolean secure) { _preprocessURL(); return super._urlWithRequestHandlerKey(requestHandlerKey, requestHandlerPath, queryString, secure); } @Override public String _urlWithRequestHandlerKey(String requestHandlerKey, String requestHandlerPath, String queryString, boolean isSecure, int somePort) { _preprocessURL(); String url = super._urlWithRequestHandlerKey(requestHandlerKey, requestHandlerPath, queryString, isSecure, somePort); url = _postprocessURL(url); return url; } /** * Returns a complete URL for the specified action. Works like * {@link WOContext#directActionURLForActionNamed} but has one extra * parameter to specify whether or not to include the current session ID * in the URL. Convenient if you embed the link for the direct * action into an email message and don't want to keep the session ID in it. * <p> * <code>actionName</code> can be either an action -- "ActionName" -- or * an action on a class -- "ActionClass/ActionName". You can also specify * <code>queryDict</code> to be an NSDictionary which contains form values * as key/value pairs. <code>includeSessionID</code> indicates if you want * to include the session ID in the URL. * * @param actionName * String action name * @param queryDict * NSDictionary containing query key/value pairs * @param includeSessionID * <code>true</code>: to include the session ID (if has one), <br> * <code>false</code>: not to include the session ID * @return a String containing the URL for the specified action * @see WODirectAction */ public String directActionURLForActionNamed(String actionName, NSDictionary queryDict, boolean includeSessionID) { String url = super.directActionURLForActionNamed(actionName, queryDict); if (!includeSessionID) { url = stripSessionIDFromURL(url); } return url; } /** * Removes session ID query key/value pair from the given URL * string. * * @param url * String URL * @return a String with the session ID removed */ public static String stripSessionIDFromURL(String url) { if (url == null) return null; String sessionIdKey = WOApplication.application().sessionIdKey(); int len = 1; int startpos = url.indexOf("?" + sessionIdKey); if (startpos < 0) { startpos = url.indexOf("&" + sessionIdKey); } if (startpos < 0) { startpos = url.indexOf("&" + sessionIdKey); len = 5; } if (startpos >= 0) { int endpos = url.indexOf('&', startpos + len); if (endpos < 0) url = url.substring(0, startpos); else { int endLen = len; if (len == 1 && url.indexOf("&") >= 0) { endLen = 5; } url = url.substring(0, startpos + len) + url.substring(endpos + endLen); } } return url; } /** * Debugging help, returns the path to current component as a list of component names. * * @param context the current context * @return an array of component names */ public static NSArray<String> componentPath(WOContext context) { NSMutableArray<String> result = new NSMutableArray<>(); if (context != null) { WOComponent component = context.component(); while (component != null) { if (component.name() != null) { result.insertObjectAtIndex(component.name(), 0); } component = component.parent(); } } return result; } /** * Debugging help, returns the path to current component as WOComponent objects. * * @param context the current context * @return an array of components */ public static NSArray<WOComponent> _componentPath(WOContext context) { NSMutableArray<WOComponent> result = new NSMutableArray<>(); if (context != null) { WOComponent component = context.component(); while (component != null) { if (component.name() != null) { result.insertObjectAtIndex(component, 0); } component = component.parent(); } } return result; } private static final String SAFE_IDENTIFIER_NAME_KEY = "ERXWOContext.safeIdentifierName"; /** * Returns a safe identifier for the current component. If willCache is true, your * component should cache the identifier name so that it does not change. In this case, * your component will be given an incrementing counter value that is unique on the * current page. If willCache is false (because you cannot cache the value), the * identifier returned will be based on the context.elementID(). While unique on the * page at any point in time, be aware that structural changes to the page can * cause the elementID of your component to change. * * @param context the WOContext * @param willCache if true, you should cache the resulting value in your component * @return a safe identifier name */ public static String safeIdentifierName(WOContext context, boolean willCache) { String safeIdentifierName; if (willCache) { NSMutableDictionary<String, Object> pageUserInfo = ERXResponseRewriter.pageUserInfo(context); Integer counter = (Integer) pageUserInfo.objectForKey(ERXWOContext.SAFE_IDENTIFIER_NAME_KEY); if (counter == null) { counter = Integer.valueOf(0); } else { counter = Integer.valueOf(counter.intValue() + 1); } pageUserInfo.setObjectForKey(counter, ERXWOContext.SAFE_IDENTIFIER_NAME_KEY); safeIdentifierName = ERXStringUtilities.safeIdentifierName(counter.toString()); } else { safeIdentifierName = ERXStringUtilities.safeIdentifierName("e_" + context.elementID()); } return safeIdentifierName; } /** * Generates direct action URLs with support for various overrides. * * @param context * the context to generate the URL within * @param directActionName * the direct action name * @param secure * <code>true</code> = https, <code>false</code> = http, <code>null</code> = same as request * @param includeSessionID * if <code>false</code>, removes session ID from query parameters * @return the constructed direct action URL */ public static String directActionUrl(WOContext context, String directActionName, Boolean secure, boolean includeSessionID) { return ERXWOContext.directActionUrl(context, null, null, null, directActionName, null, secure, includeSessionID); } /** * Generates direct action URLs with support for various overrides. * * @param context * the context to generate the URL within * @param directActionName * the direct action name * @param key * the query parameter key to add (or <code>null</code> to skip) * @param value * the query parameter value to add (or <code>null</code> to skip) * @param secure * <code>true</code> = https, <code>false</code> = http, <code>null</code> = same as request * @param includeSessionID * if <code>false</code>, removes session ID from query parameters * @return the constructed direct action URL */ public static String directActionUrl(WOContext context, String directActionName, String key, String value, Boolean secure, boolean includeSessionID) { return ERXWOContext.directActionUrl(context, null, null, null, directActionName, key, value, secure, includeSessionID); } /** * Generates direct action URLs with support for various overrides. * * @param context * the context to generate the URL within * @param directActionName * the direct action name * @param queryParameters * the query parameters to append (or <code>null</code>) * @param secure * <code>true</code> = https, <code>false</code> = http, <code>null</code> = same as request * @param includeSessionID * if <code>false</code>, removes session ID from query parameters * @return the constructed direct action URL */ public static String directActionUrl(WOContext context, String directActionName, NSDictionary<String, Object> queryParameters, Boolean secure, boolean includeSessionID) { return ERXWOContext.directActionUrl(context, null, null, null, directActionName, queryParameters, secure, includeSessionID); } /** * Generates direct action URLs with support for various overrides. * * @param context * the context to generate the URL within * @param host * the host name for the URL (or <code>null</code> for default) * @param port * the port number of the URL (or <code>null</code> for default) * @param path * the custom path prefix (or <code>null</code> for none) * @param directActionName * the direct action name * @param key * the query parameter key to add (or <code>null</code> to skip) * @param value * the query parameter value to add (or <code>null</code> to skip) * @param secure * <code>true</code> = https, <code>false</code> = http, <code>null</code> = same as request * @param includeSessionID * if <code>false</code>, removes session ID from query parameters * @return the constructed direct action URL */ public static String directActionUrl(WOContext context, String host, Integer port, String path, String directActionName, String key, Object value, Boolean secure, boolean includeSessionID) { NSDictionary<String, Object> queryParameters = null; if (key != null && value != null) { queryParameters = new NSDictionary<>(value, key); } return ERXWOContext.directActionUrl(context, host, port, path, directActionName, queryParameters, secure, includeSessionID); } /** * Generates direct action URLs with support for various overrides. * * @param context * the context to generate the URL within * @param host * the host name for the URL (or <code>null</code> for default) * @param port * the port number of the URL (or <code>null</code> for default) * @param path * the custom path prefix (or <code>null</code> for none) * @param directActionName * the direct action name * @param queryParameters * the query parameters to append (or <code>null</code>) * @param secure * <code>true</code> = https, <code>false</code> = http, <code>null</code> = same as request * @param includeSessionID * if <code>false</code>, removes session ID from query parameters * @return the constructed direct action URL */ public static String directActionUrl(WOContext context, String host, Integer port, String path, String directActionName, NSDictionary<String, Object> queryParameters, Boolean secure, boolean includeSessionID) { boolean completeUrls; boolean currentlySecure = ERXRequest.isRequestSecure(context.request()); boolean secureBool = (secure == null) ? currentlySecure : secure.booleanValue(); if (host == null && currentlySecure == secureBool && port == null) { completeUrls = true; } else { completeUrls = context.doesGenerateCompleteURLs(); } if (!completeUrls) { context.generateCompleteURLs(); } String url; try { ERXMutableURL mu = new ERXMutableURL(); boolean customPath = (path != null && path.length() > 0); if (!customPath) { mu.setURL(context._directActionURL(directActionName, queryParameters, secureBool, 0, false)); if (!includeSessionID) { mu.removeQueryParameter(WOApplication.application().sessionIdKey()); } } else { if (secureBool) { mu.setProtocol("https"); } else { mu.setProtocol("http"); } mu.setHost(context.request()._serverName()); mu.setPath(path + directActionName); mu.setQueryParameters(queryParameters); if (includeSessionID && context.session().storesIDsInURLs()) { mu.setQueryParameter(WOApplication.application().sessionIdKey(), context.session().sessionID()); } } if (port != null) { mu.setPort(port); } if (host != null && host.length() > 0) { mu.setHost(host); if (mu.protocol() == null) { if (secureBool) { mu.setProtocol("https"); } else { mu.setProtocol("http"); } } } url = mu.toExternalForm(); } catch (MalformedURLException e) { throw new RuntimeException("Failed to create url for direct action '" + directActionName + "'.", e); } finally { if (!completeUrls) { context.generateRelativeURLs(); } } return url; } public String safeElementID() { return ERXStringUtilities.safeIdentifierName(elementID()); } @Override protected String relativeURLWithRequestHandlerKey(String requestHandlerKey, String requestHandlerPath, String queryString) { String result = super.relativeURLWithRequestHandlerKey(requestHandlerKey, requestHandlerPath, queryString); if(IS_DEV && !WOApplication.application().isDirectConnectEnabled()) { String extension = "." + WOApplication.application().applicationExtension(); String replace = extension + "/-" + WOApplication.application().port(); if(!result.contains(replace) && result.contains(extension)) { result = result.replace(extension, replace); } } return result; } }