/* * 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.PrintWriter; import java.io.StringWriter; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOActionResults; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WODirectAction; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOResponse; import com.webobjects.woextensions.WOEventDisplayPage; import com.webobjects.woextensions.WOEventSetupPage; import com.webobjects.woextensions.WOStatsPage; import er.extensions.ERXExtensions; import er.extensions.components.ERXLocalizationEditor; import er.extensions.components.ERXRemoteShell; import er.extensions.components.ERXStringHolder; import er.extensions.eof.ERXDatabaseConsole; import er.extensions.eof.ERXEC; import er.extensions.eof.ERXObjectStoreCoordinator; import er.extensions.formatters.ERXUnitAwareDecimalFormat; import er.extensions.foundation.ERXConfigurationManager; import er.extensions.foundation.ERXProperties; import er.extensions.foundation.ERXStringUtilities; import er.extensions.foundation.ERXValueUtilities; import er.extensions.localization.ERXLocalizer; import er.extensions.logging.ERXLog4JConfiguration; import er.extensions.logging.ERXLogger; import er.extensions.statistics.ERXStatisticsPage; import er.extensions.statistics.ERXStats; import er.testrunner.ERXWOTestInterface; /** * Basic collector for direct action additions. All of the actions are password protected, * you need to give an argument "pw" that matches the corresponding system property for the action. */ public class ERXDirectAction extends WODirectAction { private final static Logger log = LoggerFactory.getLogger(ERXDirectAction.class); /** holds a reference to the current browser used for this session */ private ERXBrowser browser; public ERXDirectAction(WORequest r) { super(r); } /** * Checks if the action can be executed. * * @param passwordKey the password to test * @return <code>true</code> if action is allowed to be invoked */ protected boolean canPerformActionWithPasswordKey(String passwordKey) { if(ERXApplication.isDevelopmentModeSafe()) { return true; } String password = ERXProperties.decryptedStringForKey(passwordKey); if(password == null || password.length() == 0) { log.error("Attempt to use action when key is not set: {}", passwordKey); return false; } String requestPassword = request().stringFormValueForKey("pw"); if(requestPassword == null) { requestPassword = (String) context().session().objectForKey("ERXDirectAction." + passwordKey); } else { context().session().setObjectForKey(requestPassword, "ERXDirectAction." + passwordKey); } if(requestPassword == null || requestPassword.length() == 0) { return false; } return password.equals(requestPassword); } /** * Action used for junit tests. This method is only active when WOCachingEnabled is * disabled (we take this to mean that the application is not in production). * <h3>Synopsis:</h3> * pw=<i>aPassword</i>&case=<i>classNameOfTestCase</i> * <h3>Form Values:</h3> * <b>pw</b> password to be checked against the system property <code>er.extensions.ERXJUnitPassword</code>. * <b>case</b> class name for unit test to be performed. * * @return {@link er.testrunner.ERXWOTestInterface ERXWOTestInterface} * with the results after performing the given test. */ public WOActionResults testAction() { if (canPerformActionWithPasswordKey("er.extensions.ERXJUnitPassword")) { ERXWOTestInterface result = pageWithName(ERXWOTestInterface.class); session().setObjectForKey(Boolean.TRUE, "ERXWOTestInterface.enabled"); String testCase = request().stringFormValueForKey("case"); if(testCase != null) { result.theTest = testCase; // (ak:I wish we could return a direct test result...) // return (WOComponent)result.valueForKey("performTest"); } return result; } return forbiddenResponse(); } /** * Flushes the component cache to allow reloading components even when WOCachingEnabled=true. * * @return "OK" */ public WOActionResults flushComponentCacheAction() { if (canPerformActionWithPasswordKey("er.extensions.ERXFlushComponentCachePassword")) { WOApplication.application()._removeComponentDefinitionCacheContents(); return new ERXResponse("OK"); } return forbiddenResponse(); } /** * Direct access to WOStats by giving over the password in the "pw" parameter. * * @return statistics page */ public WOActionResults statsAction() { WOStatsPage nextPage = pageWithName(ERXStatisticsPage.class); nextPage.password = context().request().stringFormValueForKey("pw"); return nextPage.submit(); } /** * Direct access to reset the stats by giving over the password in the "pw" parameter. This * calls ERXStats.reset(); * * @return statistics page */ public WOActionResults resetStatsAction() { if (canPerformActionWithPasswordKey("WOStatisticsPassword")) { ERXStats.reset(); ERXRedirect redirect = pageWithName(ERXRedirect.class); redirect.setDirectActionName("stats"); redirect.setDirectActionClass("ERXDirectAction"); return redirect; } return forbiddenResponse(); } /** * Direct access to WOEventDisplay by giving over the password in the "pw" parameter. * * @return event page */ public WOActionResults eventsAction() { WOEventDisplayPage nextPage = pageWithName(WOEventDisplayPage.class); nextPage.password = context().request().stringFormValueForKey("pw"); nextPage.submit(); return nextPage; } /** * Direct access to WOEventDisplay by giving over the password in the "pw" * parameter and turning on all events. * * @return event setup page */ public WOActionResults eventsSetupAction() { WOEventSetupPage nextPage = pageWithName(WOEventSetupPage.class); nextPage.password = context().request().stringFormValueForKey("pw"); nextPage.submit(); nextPage.selectAll(); return eventsAction(); } /** * Action used for turning EOAdaptorDebugging output on or off. * <h3>Synopsis:</h3> * pw=<i>aPassword</i> * <h3>Form Values:</h3> * <strong>pw</strong> password to be checked against the system property <code>er.extensions.ERXEOAdaptorDebuggingPassword</code>.<br> * <strong>debug</strong> flag signaling whether to turn EOAdaptorDebugging on or off (defaults to off). The value should be one of: * <ul> * <li>on</li> * <li>true</li> * <li>1</li> * <li>y</li> * <li>yes</li> * <li>off</li> * <li>false</li> * <li>0</li> * <li>n</li> * <li>no</li> * </ul> * <p> * Note: this action must be invoked against a specific instance (the instance number must be in the request URL). * * @return a page showing what action was taken (with regard to EOAdaptorDebugging), if any. */ public WOActionResults eoAdaptorDebuggingAction() { if (canPerformActionWithPasswordKey("er.extensions.ERXEOAdaptorDebuggingPassword")) { ERXStringHolder result = pageWithName(ERXStringHolder.class); result.setEscapeHTML(false); String message; boolean currentState = ERXExtensions.adaptorLogging(); int instance = request().applicationNumber(); if (instance == -1) { log.info("EOAdaptorDebuggingAction requested without a specific instance."); message = "<p>You must invoke this action on a <em>specific</em> instance.</p>" + "<p>Your url should look like: <code>.../WebObjects/1/wa/...</code>, where '1' would be the first instance of the target application.</p>"; } else { String debugParam = request().stringFormValueForKey("debug"); log.debug("EOAdaptorDebuggingAction requested with 'debug' param: {}", debugParam); if (debugParam == null || debugParam.trim().length() == 0) { message = "<p>EOAdaptorDebugging is currently <strong>" + (currentState ? "ON" : "OFF") + "</strong> for instance <strong>" + instance + "</strong>.</p>"; message += "<p>To change the setting, provide the 'debug' parameter to this action, e.g.: <code>...eoAdaptorDebugging?debug=on&pw=secret</code></p>"; } else { if (debugParam.trim().equalsIgnoreCase("on")) { debugParam = "true"; } else if (debugParam.trim().equalsIgnoreCase("off")) { debugParam = "false"; } boolean desiredState = ERXValueUtilities.booleanValueWithDefault(debugParam, false); log.debug("EOAdaptorDebuggingAction requested 'debug' state change to: '{}' for instance: {}.", desiredState, instance); if (currentState != desiredState) { ERXExtensions.setAdaptorLogging(desiredState); message = "<p>Turned EOAdaptorDebugging <strong>" + (desiredState ? "ON" : "OFF") + "</strong> for instance <strong>" + instance + "</strong>.</p>"; } else { message = "<p>EOAdaptorDebugging setting <strong>not changed</strong>.</p>"; } } } message += "<p><em>Please be mindful of using EOAdaptorDebugging as it may have a large impact on application performance.</em></p>"; result.setValue(message); return result; } return forbiddenResponse(); } /** * Action used for changing logging settings at runtime. This method is only active * when WOCachingEnabled is disabled (we take this to mean that the application is * not in production). * <h3>Synopsis:</h3> * pw=<i>aPassword</i> * <h3>Form Values:</h3> * <b>pw</b> password to be checked against the system property <code>er.extensions.ERXLog4JPassword</code>. * * @return {@link ERXLog4JConfiguration} for modifying current logging settings. */ public WOActionResults log4jAction() { if (canPerformActionWithPasswordKey("er.extensions.ERXLog4JPassword")) { session().setObjectForKey(Boolean.TRUE, "ERXLog4JConfiguration.enabled"); return pageWithName(ERXLog4JConfiguration.class); } return forbiddenResponse(); } /** * Action used for sending shell commands to the server and receive the result * <h3>Synopsis:</h3> * pw=<i>aPassword</i> * <h3>Form Values:</h3> * <b>pw</b> password to be checked against the system property <code>er.extensions.ERXRemoteShellPassword</code>. * * @return {@link ERXLog4JConfiguration} for modifying current logging settings. */ public WOActionResults remoteShellAction() { if (canPerformActionWithPasswordKey("er.extensions.ERXRemoteShellPassword")) { session().setObjectForKey(Boolean.TRUE, "ERXRemoteShell.enabled"); return pageWithName(ERXRemoteShell.class); } return forbiddenResponse(); } /** * Action used for accessing the database console * <h3>Synopsis:</h3> * pw=<i>aPassword</i> * <h3>Form Values:</h3> * <b>pw</b> password to be checked against the system property <code>er.extensions.ERXRemoteShellPassword</code>. * * @return {@link ERXLog4JConfiguration} for modifying current logging settings. */ public WOActionResults databaseConsoleAction() { if (canPerformActionWithPasswordKey("er.extensions.ERXDatabaseConsolePassword")) { session().setObjectForKey(Boolean.TRUE, "ERXDatabaseConsole.enabled"); return pageWithName(ERXDatabaseConsole.class); } return forbiddenResponse(); } /** * Action used for forcing garbage collection. If WOCachingEnabled is true (we take this to mean * that the application is in production) you need to give a password to access it. * <h3>Synopsis:</h3> * pw=<i>aPassword</i> * <h3>Form Values:</h3> * <b>pw</b> password to be checked against the system property <code>er.extensions.ERXGCPassword</code>. * * @return short info about free and used memory before and after GC. */ public WOActionResults forceGCAction() { if (canPerformActionWithPasswordKey("er.extensions.ERXGCPassword")) { ERXStringHolder result = pageWithName(ERXStringHolder.class); Runtime runtime = Runtime.getRuntime(); ERXUnitAwareDecimalFormat decimalFormatter = new ERXUnitAwareDecimalFormat(ERXUnitAwareDecimalFormat.BYTE); decimalFormatter.setMaximumFractionDigits(2); String info = "Before: "; info += decimalFormatter.format(runtime.maxMemory()) + " max, "; info += decimalFormatter.format(runtime.totalMemory()) + " total, "; info += decimalFormatter.format(runtime.totalMemory()-runtime.freeMemory()) + " used, "; info += decimalFormatter.format(runtime.freeMemory()) + " free\n"; int count = 5; if(request().stringFormValueForKey("count") != null) { count = Integer.parseInt(request().stringFormValueForKey("count")); } ERXExtensions.forceGC(count); info += "After: "; info += decimalFormatter.format(runtime.maxMemory()) + " max, "; info += decimalFormatter.format(runtime.totalMemory()) + " total, "; info += decimalFormatter.format(runtime.totalMemory()-runtime.freeMemory()) + " used, "; info += decimalFormatter.format(runtime.freeMemory()) + " free\n"; result.setValue(info); log.info("GC forced\n{}", info); return result; } return forbiddenResponse(); } /** * Returns a list of the traces of open editing context locks. This is only useful if * er.extensions.ERXEC.traceOpenLocks is enabled and * er.extensions.ERXOpenEditingContextLocksPassword is set. * * @return list of lock traces */ public WOActionResults showOpenEditingContextLockTracesAction() { if (canPerformActionWithPasswordKey("er.extensions.ERXOpenEditingContextLockTracesPassword")) { ERXStringHolder result = pageWithName(ERXStringHolder.class); result.setEscapeHTML(false); try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { pw.println("<pre>"); pw.println(ERXEC.outstandingLockDescription()); pw.println("</pre>"); pw.println("<hr>"); pw.println("<pre>"); pw.println(ERXObjectStoreCoordinator.outstandingLockDescription()); pw.println("</pre>"); result.setValue(sw.toString()); } catch (IOException e) { // ignore } return result; } return forbiddenResponse(); } /** * Will terminate an existing session and redirect to the default action. * * @return redirect to default action */ public WOActionResults logoutAction() { if (existingSession()!=null) { existingSession().terminate(); } ERXRedirect r = pageWithName(ERXRedirect.class); r.setDirectActionName("default"); return r; } /** * 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 && request() != null) { ERXBrowserFactory browserFactory = ERXBrowserFactory.factory(); browser = browserFactory.browserMatchingRequest(request()); browserFactory.retainBrowser(browser); } return browser; } @Override public WOActionResults performActionNamed(String actionName) { WOActionResults actionResult = super.performActionNamed(actionName); if (browser != null) ERXBrowserFactory.factory().releaseBrowser(browser); return actionResult; } /** * Sets a System property. This is also active in deployment mode because one might want to change a System property * at runtime. * <h3>Synopsis:</h3> * pw=<i>aPassword</i>&key=<i>someSystemPropertyKey</i>&value=<i>someSystemPropertyValue</i> * * @return either null when the password is wrong or a new page showing the System properties */ public WOActionResults systemPropertyAction() { if (canPerformActionWithPasswordKey("er.extensions.ERXDirectAction.ChangeSystemPropertyPassword")) { String key = request().stringFormValueForKey("key"); ERXResponse r = new ERXResponse(); if (ERXStringUtilities.stringIsNullOrEmpty(key) ) { String user = request().stringFormValueForKey("user"); Properties props = ERXConfigurationManager.defaultManager().defaultProperties(); if(user != null) { System.setProperty("user.name", user); props = ERXConfigurationManager.defaultManager().applyConfiguration(props); } r.appendContentString(ERXProperties.logString(props)); } else { String value = request().stringFormValueForKey("value"); value = ERXStringUtilities.stringIsNullOrEmpty(value) ? "" : value; java.util.Properties p = System.getProperties(); p.put(key, value); System.setProperties(p); ERXLogger.configureLoggingWithSystemProperties(); for (java.util.Enumeration e = p.keys(); e.hasMoreElements();) { Object k = e.nextElement(); if (k.equals(key)) { r.appendContentString("<b>'"+k+"="+p.get(k)+"' <= you changed this</b><br>"); } else { r.appendContentString("'"+k+"="+p.get(k)+"'<br>"); } } r.appendContentString("</body></html>"); } return r; } return forbiddenResponse(); } /** * Opens the localizer edit page if the app is in development mode. * * @return localizer editor */ public WOActionResults editLocalizedFilesAction() { if (ERXApplication.isDevelopmentModeSafe()) { return pageWithName(ERXLocalizationEditor.class); } return null; } /** * Will dump all created keys of the current localizer via log4j and * returns an empty response. * * @return empty response */ public WOActionResults dumpCreatedKeysAction() { if (ERXApplication.isDevelopmentModeSafe()) { session(); ERXLocalizer.currentLocalizer().dumpCreatedKeys(); return new ERXResponse(); } return null; } /** * Returns an empty response. * * @return nothing */ public WOActionResults emptyAction() { return new ERXResponse(); } /** * To use this, include this line in appendToResponse on any pages with uploads: * <code> * AjaxUtils.addScriptResourceInHead(context, response, "Ajax", "prototype.js"); * AjaxUtils.addScriptResourceInHead(context, response, "Ajax", "SafariUploadHack.js"); * </code> * * <p>To be called before multi-form submits to get past Safari 3.2.1 and 4.x intermittent hang-ups * when posting binary data. A nice succinct description and solution is posted here: * http://blog.airbladesoftware.com/2007/8/17/note-to-self-prevent-uploads-hanging-in-safari * The radar ticket is here: https://bugs.webkit.org/show_bug.cgi?id=5760</p> * * @return simple response to close the connection */ public WOActionResults closeHTTPSessionAction() { ERXResponse response = new ERXResponse(""); response.setHeader("close", "Connection"); return response; } @SuppressWarnings("unchecked") public <T extends WOComponent> T pageWithName(Class<T> componentClass) { return (T) super.pageWithName(componentClass.getName()); } /** * Terminates the application when in development. * * @return "OK" if application has been shut down */ public WOActionResults stopAction() { ERXResponse response = new ERXResponse(); response.setHeader("text/plain", "Content-Type"); if (ERXApplication.isDevelopmentModeSafe()) { WOApplication.application().terminate(); response.setContent("OK"); } else { response.setStatus(401); } return response; } /** * Creates a response object with HTTP status code 403. * * @return 403 response */ protected WOResponse forbiddenResponse() { return new ERXResponse(null, ERXHttpStatusCodes.FORBIDDEN); } }