瀏覽代碼

peoplesearch orgchart

jrivard 10 年之前
父節點
當前提交
f1c05d049c
共有 30 個文件被更改,包括 1103 次插入301 次删除
  1. 246 123
      pwm/servlet/src/password/pwm/PwmApplication.java
  2. 2 0
      pwm/servlet/src/password/pwm/PwmConstants.java
  3. 2 1
      pwm/servlet/src/password/pwm/PwmConstants.properties
  4. 4 0
      pwm/servlet/src/password/pwm/config/PwmSetting.java
  5. 14 0
      pwm/servlet/src/password/pwm/config/PwmSetting.xml
  6. 7 1
      pwm/servlet/src/password/pwm/config/function/UserMatchViewerFunction.java
  7. 8 0
      pwm/servlet/src/password/pwm/error/ErrorInformation.java
  8. 1 0
      pwm/servlet/src/password/pwm/error/PwmError.java
  9. 12 2
      pwm/servlet/src/password/pwm/health/LDAPStatusChecker.java
  10. 89 41
      pwm/servlet/src/password/pwm/http/ContextManager.java
  11. 12 0
      pwm/servlet/src/password/pwm/http/filter/RequestInitializationFilter.java
  12. 7 1
      pwm/servlet/src/password/pwm/http/servlet/ConfigGuideServlet.java
  13. 1 1
      pwm/servlet/src/password/pwm/http/servlet/ForgottenPasswordServlet.java
  14. 251 45
      pwm/servlet/src/password/pwm/http/servlet/PeopleSearchServlet.java
  15. 1 0
      pwm/servlet/src/password/pwm/i18n/Display.properties
  16. 1 0
      pwm/servlet/src/password/pwm/i18n/Error.properties
  17. 11 0
      pwm/servlet/src/password/pwm/util/cache/CacheService.java
  18. 21 6
      pwm/servlet/src/password/pwm/util/cli/MainClass.java
  19. 157 0
      pwm/servlet/src/password/pwm/util/cli/ResponseStatsCommand.java
  20. 24 13
      pwm/servlet/src/password/pwm/util/report/ReportService.java
  21. 1 1
      pwm/servlet/src/password/pwm/wordlist/SeedlistManager.java
  22. 1 1
      pwm/servlet/src/password/pwm/wordlist/WordlistManager.java
  23. 11 3
      pwm/servlet/web/WEB-INF/jsp/application-unavailable.jsp
  24. 1 1
      pwm/servlet/web/WEB-INF/jsp/error-http.jsp
  25. 1 10
      pwm/servlet/web/WEB-INF/jsp/error.jsp
  26. 2 2
      pwm/servlet/web/WEB-INF/web.xml
  27. 22 19
      pwm/servlet/web/public/resources/js/configeditor-settings.js
  28. 15 3
      pwm/servlet/web/public/resources/js/main.js
  29. 155 26
      pwm/servlet/web/public/resources/js/peoplesearch.js
  30. 23 1
      pwm/servlet/web/public/resources/style.css

+ 246 - 123
pwm/servlet/src/password/pwm/PwmApplication.java

@@ -64,6 +64,8 @@ import password.pwm.wordlist.SharedHistoryManager;
 import password.pwm.wordlist.WordlistManager;
 
 import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.util.*;
 
 /**
@@ -79,7 +81,6 @@ public class PwmApplication {
     private static final PwmLogger LOGGER = PwmLogger.forClass(PwmApplication.class);
     private static final String DEFAULT_INSTANCE_ID = "-1";
 
-
     public enum AppAttribute {
         INSTANCE_ID("context_instanceID"),
         INSTALL_DATE("DB_KEY_INSTALL_DATE"),
@@ -96,7 +97,7 @@ public class PwmApplication {
 
         private String key;
 
-        private AppAttribute(String key) {
+        AppAttribute(String key) {
             this.key = key;
         }
 
@@ -117,8 +118,11 @@ public class PwmApplication {
     private final Date startupTime = new Date();
     private Date installTime = new Date();
     private ErrorInformation lastLocalDBFailure = null;
-    private File applicationPath;
-    private File configurationFile;
+
+    private final PwmEnvironment pwmEnvironment;
+    private final File applicationPath;
+    private final File webInfPath;
+    private final File configurationFile;
 
     private MODE applicationMode;
 
@@ -144,129 +148,31 @@ public class PwmApplication {
     ));
 
 
-    public PwmApplication(
-            final Configuration config,
-            final MODE applicationMode,
-            final File applicationPath,
-            final boolean initLogging,
-            final File configurationFile
-    )
-    {
-        this.configuration = config;
-        this.applicationMode = applicationMode;
-        this.applicationPath = applicationPath;
-        this.configurationFile = configurationFile;
-        initialize(initLogging);
-    }
-
-// --------------------- GETTER / SETTER METHODS ---------------------
-
-    public String getInstanceID() {
-        return instanceID;
-    }
-
-    public SharedHistoryManager getSharedHistoryManager() {
-        return (SharedHistoryManager)pwmServices.get(SharedHistoryManager.class);
-    }
-
-    public IntruderManager getIntruderManager() {
-        return (IntruderManager)pwmServices.get(IntruderManager.class);
-    }
-
-    public ChaiUser getProxiedChaiUser(final UserIdentity userIdentity)
-            throws ChaiUnavailableException, PwmUnrecoverableException
-    {
-        final ChaiProvider proxiedProvider = getProxyChaiProvider(userIdentity.getLdapProfileID());
-        return ChaiFactory.createChaiUser(userIdentity.getUserDN(), proxiedProvider);
-
-    }
-
-    public ChaiProvider getProxyChaiProvider(final String identifier)
+    private PwmApplication(final PwmEnvironment pwmEnvironment)
             throws PwmUnrecoverableException
     {
-        return getLdapConnectionService().getProxyChaiProvider(identifier);
-    }
-
-    public LocalDBLogger getLocalDBLogger() {
-        return localDBLogger;
-    }
-
-    public HealthMonitor getHealthMonitor() {
-        return (HealthMonitor)pwmServices.get(HealthMonitor.class);
-    }
-
-    public List<PwmService> getPwmServices() {
-        final List<PwmService> pwmServices = new ArrayList<>();
-        pwmServices.add(this.localDBLogger);
-        pwmServices.addAll(this.pwmServices.values());
-        pwmServices.remove(null);
-        return Collections.unmodifiableList(pwmServices);
-    }
-
-    public WordlistManager getWordlistManager() {
-        return (WordlistManager)pwmServices.get(WordlistManager.class);
-    }
-
-    public SeedlistManager getSeedlistManager() {
-        return (SeedlistManager)pwmServices.get(SeedlistManager.class);
-    }
-
-    public ReportService getUserReportService() {
-        return (ReportService)pwmServices.get(ReportService.class);
-    }
-
-    public EmailQueueManager getEmailQueue() {
-        return (EmailQueueManager)pwmServices.get(EmailQueueManager.class);
-    }
-
-    public AuditManager getAuditManager() {
-        return (AuditManager)pwmServices.get(AuditManager.class);
-    }
+        this.pwmEnvironment = pwmEnvironment;
+        this.configuration = pwmEnvironment.config;
+        this.applicationMode = pwmEnvironment.applicationMode;
+        this.applicationPath = pwmEnvironment.applicationPath;
+        this.configurationFile = pwmEnvironment.configurationFile;
+        this.webInfPath = pwmEnvironment.webInfPath;
 
-    public SmsQueueManager getSmsQueue() {
-        return (SmsQueueManager)pwmServices.get(SmsQueueManager.class);
-    }
-
-    public UrlShortenerService getUrlShortener() {
-        return (UrlShortenerService)pwmServices.get(UrlShortenerService.class);
-    }
-
-    public VersionChecker getVersionChecker() {
-        return (VersionChecker)pwmServices.get(VersionChecker.class);
-    }
-
-    public ErrorInformation getLastLocalDBFailure() {
-        return lastLocalDBFailure;
-    }
-
-
-    public TokenService getTokenService() {
-        return (TokenService)pwmServices.get(TokenService.class);
-    }
-
-    public LdapConnectionService getLdapConnectionService() {
-        return (LdapConnectionService)pwmServices.get(LdapConnectionService.class);
-    }
-
-    public Configuration getConfig() {
-        if (configuration == null) {
-            return null;
+        try {
+            initialize(pwmEnvironment.initLogging);
+        } catch (PwmUnrecoverableException e) {
+            LOGGER.fatal(e.getMessage());
+            throw e;
         }
-        return configuration;
-    }
-
-    public MODE getApplicationMode() {
-        return applicationMode;
     }
 
-    public synchronized DatabaseAccessorImpl getDatabaseAccessor()
+    private void initialize(final boolean initLogging)
+            throws PwmUnrecoverableException
     {
-        return (DatabaseAccessorImpl)pwmServices.get(DatabaseAccessorImpl.class);
-    }
-
-    private void initialize(final boolean initLogging) {
         final Date startTime = new Date();
 
+        verifyIfApplicationPathIsSetProperly(pwmEnvironment);
+
         // initialize log4j
         if (initLogging) {
             final String log4jFileName = configuration.readSettingAsString(PwmSetting.EVENTS_JAVA_LOG4JCONFIG_FILE);
@@ -302,8 +208,8 @@ public class PwmApplication {
         }
 
         LOGGER.info("initializing, application mode=" + getApplicationMode()
-                + ", applicationPath=" + (applicationPath == null ? "null" : applicationPath.getAbsolutePath())
-                + ", configurationFile=" + (configurationFile == null ? "null" : configurationFile.getAbsolutePath())
+                        + ", applicationPath=" + (applicationPath == null ? "null" : applicationPath.getAbsolutePath())
+                        + ", configurationFile=" + (configurationFile == null ? "null" : configurationFile.getAbsolutePath())
         );
 
         this.localDB = Initializer.initializeLocalDB(this);
@@ -323,7 +229,7 @@ public class PwmApplication {
         LOGGER.info(logEnvironment());
         LOGGER.info(logDebugInfo());
 
-        for (final Class serviceClass : PWM_SERVICE_CLASSES) {
+        for (final Class<? extends PwmService> serviceClass : PWM_SERVICE_CLASSES) {
             final PwmService newServiceInstance;
             try {
                 final Object newInstance = serviceClass.newInstance();
@@ -331,7 +237,7 @@ public class PwmApplication {
             } catch (Exception e) {
                 final String errorMsg = "unexpected error instantiating service class '" + serviceClass.getName() + "', error: " + e.toString();
                 LOGGER.fatal(errorMsg,e);
-                throw new IllegalStateException(errorMsg);
+                throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR,errorMsg));
             }
 
             try {
@@ -346,7 +252,7 @@ public class PwmApplication {
                     errorMsg += ", cause: " + e.getCause();
                 }
                 LOGGER.fatal(errorMsg);
-                throw new IllegalStateException(errorMsg,e);
+                throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR,errorMsg));
             }
             pwmServices.put(serviceClass,newServiceInstance);
         }
@@ -402,6 +308,107 @@ public class PwmApplication {
         }
     }
 
+    public String getInstanceID() {
+        return instanceID;
+    }
+
+    public SharedHistoryManager getSharedHistoryManager() {
+        return (SharedHistoryManager)pwmServices.get(SharedHistoryManager.class);
+    }
+
+    public IntruderManager getIntruderManager() {
+        return (IntruderManager)pwmServices.get(IntruderManager.class);
+    }
+
+    public ChaiUser getProxiedChaiUser(final UserIdentity userIdentity)
+            throws ChaiUnavailableException, PwmUnrecoverableException
+    {
+        final ChaiProvider proxiedProvider = getProxyChaiProvider(userIdentity.getLdapProfileID());
+        return ChaiFactory.createChaiUser(userIdentity.getUserDN(), proxiedProvider);
+    }
+
+    public ChaiProvider getProxyChaiProvider(final String identifier)
+            throws PwmUnrecoverableException
+    {
+        return getLdapConnectionService().getProxyChaiProvider(identifier);
+    }
+
+    public LocalDBLogger getLocalDBLogger() {
+        return localDBLogger;
+    }
+
+    public HealthMonitor getHealthMonitor() {
+        return (HealthMonitor)pwmServices.get(HealthMonitor.class);
+    }
+
+    public List<PwmService> getPwmServices() {
+        final List<PwmService> pwmServices = new ArrayList<>();
+        pwmServices.add(this.localDBLogger);
+        pwmServices.addAll(this.pwmServices.values());
+        pwmServices.remove(null);
+        return Collections.unmodifiableList(pwmServices);
+    }
+
+    public WordlistManager getWordlistManager() {
+        return (WordlistManager)pwmServices.get(WordlistManager.class);
+    }
+
+    public SeedlistManager getSeedlistManager() {
+        return (SeedlistManager)pwmServices.get(SeedlistManager.class);
+    }
+
+    public ReportService getUserReportService() {
+        return (ReportService)pwmServices.get(ReportService.class);
+    }
+
+    public EmailQueueManager getEmailQueue() {
+        return (EmailQueueManager)pwmServices.get(EmailQueueManager.class);
+    }
+
+    public AuditManager getAuditManager() {
+        return (AuditManager)pwmServices.get(AuditManager.class);
+    }
+
+    public SmsQueueManager getSmsQueue() {
+        return (SmsQueueManager)pwmServices.get(SmsQueueManager.class);
+    }
+
+    public UrlShortenerService getUrlShortener() {
+        return (UrlShortenerService)pwmServices.get(UrlShortenerService.class);
+    }
+
+    public VersionChecker getVersionChecker() {
+        return (VersionChecker)pwmServices.get(VersionChecker.class);
+    }
+
+    public ErrorInformation getLastLocalDBFailure() {
+        return lastLocalDBFailure;
+    }
+
+    public TokenService getTokenService() {
+        return (TokenService)pwmServices.get(TokenService.class);
+    }
+
+    public LdapConnectionService getLdapConnectionService() {
+        return (LdapConnectionService)pwmServices.get(LdapConnectionService.class);
+    }
+
+    public Configuration getConfig() {
+        if (configuration == null) {
+            return null;
+        }
+        return configuration;
+    }
+
+    public MODE getApplicationMode() {
+        return applicationMode;
+    }
+
+    public synchronized DatabaseAccessorImpl getDatabaseAccessor()
+    {
+        return (DatabaseAccessorImpl)pwmServices.get(DatabaseAccessorImpl.class);
+    }
+
     private Date fetchInstallDate(final Date startupTime) {
         if (localDB != null) {
             try {
@@ -662,6 +669,122 @@ public class PwmApplication {
             LOGGER.error("error retrieving key '" + appAttribute.getKey() + "' installation date from localDB: " + e.getMessage());
         }
     }
+
+    public File getWebInfPath() {
+        return webInfPath;
+    }
+
+    private static void verifyIfApplicationPathIsSetProperly(final PwmEnvironment pwmEnvironment)
+            throws PwmUnrecoverableException
+    {
+        final File webInfPath = pwmEnvironment.webInfPath;
+        final File applicationPath = pwmEnvironment.applicationPath;
+
+        if (applicationPath == null) {
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR, "unable to determine valid applicationPath"));
+        }
+
+        if (!applicationPath.exists()) {
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR, "applicationPath \"" + applicationPath.getAbsolutePath() + "\" does not exist"));
+        }
+
+        if (!applicationPath.canRead()) {
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR, "unable to read from applicationPath \"" + applicationPath.getAbsolutePath() + "\""));
+        }
+
+        if (!applicationPath.canWrite()) {
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR, "unable to write to applicationPath \"" + applicationPath.getAbsolutePath() + "\""));
+        }
+
+        if (webInfPath == null) {
+            return;
+        }
+
+        final File infoFile = new File(webInfPath.getAbsolutePath() + File.separator + PwmConstants.APPLICATION_PATH_INFO_FILE);
+        if (pwmEnvironment.applicationPathType == PwmEnvironment.ApplicationPathType.derived) {
+            LOGGER.trace("checking " + infoFile.getAbsolutePath() + " status, (applicationPath=" + PwmEnvironment.ApplicationPathType.derived);
+            if (infoFile.exists()) {
+                final String errorMsg = "The file \"" + infoFile.getAbsolutePath() + "\" exists, but applicationPath was not explicitly specified."
+                        + "  This file must be removed, or an explicit applicationPath parameter must be specified.";
+                throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_STARTUP_ERROR,errorMsg));
+            } else {
+                LOGGER.trace(infoFile.getAbsolutePath() + " does not exist");
+            }
+        }
+
+        if (webInfPath.equals(applicationPath)) {
+            LOGGER.trace("webInfPath and applicationPath are same");
+            return;
+        }
+
+        if (pwmEnvironment.applicationPathType == PwmEnvironment.ApplicationPathType.specified) {
+            try {
+                final FileOutputStream fos = new FileOutputStream(infoFile);
+                final Properties outputProperties = new Properties();
+                outputProperties.setProperty("lastApplicationPath", applicationPath.getAbsolutePath());
+                outputProperties.store(fos, "Marker file to record a previously specified applicationPath");
+            } catch (IOException e) {
+                LOGGER.warn("unable to write marker properties file in WEB-INF directory");
+            }
+        }
+    }
+
+    public static class PwmEnvironment {
+        private MODE applicationMode = MODE.ERROR;
+
+        private Configuration config;
+        private File applicationPath;
+        private boolean initLogging;
+        private File configurationFile;
+        private File webInfPath;
+        private ApplicationPathType applicationPathType = ApplicationPathType.derived;
+
+        public enum ApplicationPathType {
+            derived,
+            specified,
+        }
+
+        public PwmEnvironment setConfig(Configuration config) {
+            this.config = config;
+            return this;
+        }
+
+        public PwmEnvironment setApplicationMode(MODE applicationMode) {
+            this.applicationMode = applicationMode;
+            return this;
+        }
+
+        public PwmEnvironment setApplicationPath(File applicationPath) {
+            this.applicationPath = applicationPath;
+            return this;
+        }
+
+        public PwmEnvironment setInitLogging(boolean initLogging) {
+            this.initLogging = initLogging;
+            return this;
+        }
+
+        public PwmEnvironment setConfigurationFile(File configurationFile) {
+            this.configurationFile = configurationFile;
+            return this;
+        }
+
+        public PwmEnvironment setWebInfPath(File webInfPath) {
+            this.webInfPath = webInfPath;
+            return this;
+        }
+
+        public PwmEnvironment setApplicationPathType(ApplicationPathType applicationPathType) {
+            this.applicationPathType = applicationPathType;
+            return this;
+        }
+
+        public PwmApplication createPwmApplication()
+                throws PwmUnrecoverableException
+        {
+            return new PwmApplication(this);
+        }
+    }
 }
 
 

+ 2 - 0
pwm/servlet/src/password/pwm/PwmConstants.java

@@ -91,6 +91,8 @@ public abstract class PwmConstants {
         DEFAULT_DATETIME_FORMAT.setTimeZone(DEFAULT_TIMEZONE);
     }
 
+    public static final String APPLICATION_PATH_INFO_FILE = readPwmConstantsBundle("applicationPathInfoFile");
+
     public static final int DEFAULT_WORDLIST_LOADFACTOR = Integer.parseInt(readPwmConstantsBundle("wordlist.loadFactor"));
     public static final int LOCALDB_LOGGER_MAX_QUEUE_SIZE = Integer.parseInt(readPwmConstantsBundle("pwmDBLoggerMaxQueueSize"));
     public static final int LOCALDB_LOGGER_MAX_DIRTY_BUFFER_MS = Integer.parseInt(readPwmConstantsBundle("pwmDBLoggerMaxDirtyBufferMS"));

+ 2 - 1
pwm/servlet/src/password/pwm/PwmConstants.properties

@@ -46,4 +46,5 @@ pwmDBLoggerMaxQueueSize=50000
 pwmDBLoggerMaxDirtyBufferMS=500
 enableEulaDisplay=false
 databaseAccessor.keyLength=128
-missingVersionString=[Missing_Version_#]
+missingVersionString=[Missing_Version_#]
+applicationPathInfoFile=applicationPath.properties

+ 4 - 0
pwm/servlet/src/password/pwm/config/PwmSetting.java

@@ -810,6 +810,10 @@ public enum PwmSetting {
             "peopleSearch.enablePublic", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PEOPLE_SEARCH),
     PEOPLE_SEARCH_IDLE_TIMEOUT_SECONDS(
             "peopleSearch.idleTimeout", PwmSettingSyntax.DURATION, PwmSettingCategory.PEOPLE_SEARCH),
+    PEOPLE_SEARCH_ORGCHART_PARENT_ATTRIBUTE(
+            "peopleSearch.orgChart.parentAttribute", PwmSettingSyntax.STRING, PwmSettingCategory.PEOPLE_SEARCH),
+    PEOPLE_SEARCH_ORGCHART_CHILD_ATTRIBUTE(
+            "peopleSearch.orgChart.childAttribute", PwmSettingSyntax.STRING, PwmSettingCategory.PEOPLE_SEARCH),
 
 
 

+ 14 - 0
pwm/servlet/src/password/pwm/config/PwmSetting.xml

@@ -3041,6 +3041,20 @@
             <value>0</value>
         </default>
     </setting>
+    <setting key="peopleSearch.orgChart.parentAttribute" level="1">
+        <label>Org Chart Parent Attribute</label>
+        <description/>
+        <default>
+            <value>manager</value>
+        </default>
+    </setting>
+    <setting key="peopleSearch.orgChart.childAttribute" level="1">
+        <label>Org Chart Child Attribute</label>
+        <description/>
+        <default>
+            <value>directReports</value>
+        </default>
+    </setting>
     <setting key="ldap.edirectory.enableNmas" level="1" required="true">
         <label>Enable NMAS Extensions</label>
         <description><![CDATA[When connecting to a NetIQ eDirectory LDAP directory, this parameter will control if NMAS extensions will be used when connecting to the ldap directory.  Enabling nmas results in:<ul><li>better error messages when using universal password policies</li><li>better error handling during certain change password scenarios</li></ul>Unless you are using an older version of eDirectory (pre 8.8 or before), it is generally best to set this to true.<br/><br/>All NMAS operations require an SSL connection to the directory.]]></description>

+ 7 - 1
pwm/servlet/src/password/pwm/config/function/UserMatchViewerFunction.java

@@ -72,7 +72,13 @@ public class UserMatchViewerFunction implements SettingUIFunction {
             throws Exception
     {
         final Configuration config = new Configuration(storedConfiguration);
-        final PwmApplication tempApplication = new PwmApplication(config, PwmApplication.MODE.CONFIGURATION, null, false, null);
+        final PwmApplication tempApplication = new PwmApplication.PwmEnvironment()
+                .setConfig(config)
+                .setApplicationMode(PwmApplication.MODE.CONFIGURATION)
+                .setApplicationPath(null).setInitLogging(false)
+                .setConfigurationFile(null)
+                .setWebInfPath(null)
+                .createPwmApplication();
         final List<UserPermission> permissions = (List<UserPermission>)storedConfiguration.readSetting(setting,profile).toNativeObject();
 
         for (final UserPermission userPermission : permissions) {

+ 8 - 0
pwm/servlet/src/password/pwm/error/ErrorInformation.java

@@ -149,4 +149,12 @@ public class ErrorInformation implements Serializable {
     public Date getDate() {
         return date;
     }
+
+    public ErrorInformation wrapWithNewErrorCode(final PwmError pwmError) {
+        if (pwmError == this.getError()) {
+            return this;
+        }
+        return new ErrorInformation(pwmError,this.getDetailedErrorMsg());
+
+    }
 }

+ 1 - 0
pwm/servlet/src/password/pwm/error/PwmError.java

@@ -158,6 +158,7 @@ public enum PwmError {
     ERROR_LDAP_DATA_ERROR("Error_LdapDataError",5079, true),
     ERROR_MACRO_PARSE_ERROR("Error_MacroParseError",5080,true),
     ERROR_NO_PROFILE_ASSIGNED("Error_NoProfileAssigned",5081,true),
+    ERROR_STARTUP_ERROR("Error_StartupError",5082,true),
 
     ERROR_FIELD_REQUIRED("Error_FieldRequired", 5100, false),
     ERROR_FIELD_NOT_A_NUMBER("Error_FieldNotANumber", 5101, false),

+ 12 - 2
pwm/servlet/src/password/pwm/health/LDAPStatusChecker.java

@@ -542,8 +542,18 @@ public class LDAPStatusChecker implements HealthChecker {
             boolean testContextless,
             boolean fullTest
 
-    ) {
-        final PwmApplication tempApplication = new PwmApplication(config, PwmApplication.MODE.NEW, null, false, null);
+    )
+                throws PwmUnrecoverableException
+    {
+        final PwmApplication tempApplication = new PwmApplication.PwmEnvironment()
+                .setConfig(config)
+                .setApplicationMode(PwmApplication.MODE.NEW)
+                .setApplicationPath(null)
+                .setInitLogging(false)
+                .setConfigurationFile(null)
+                .setWebInfPath(null)
+                .createPwmApplication();
+
         final LDAPStatusChecker ldapStatusChecker = new LDAPStatusChecker();
         final List<HealthRecord> profileRecords = new ArrayList<>();
 

+ 89 - 41
pwm/servlet/src/password/pwm/http/ContextManager.java

@@ -141,11 +141,25 @@ public class ContextManager implements Serializable {
         }
 
         Configuration configuration = null;
-        File applicationPath = null;
         PwmApplication.MODE mode = PwmApplication.MODE.ERROR;
+
+        final File webInfPath = locateWebInfFilePath();
+
+        final File applicationPath;
+        final PwmApplication.PwmEnvironment.ApplicationPathType applicationPathType;
+        {
+            final String applicationPathStr = readSpecifiedApplicationPath();
+            if (applicationPathStr == null || applicationPathStr.isEmpty()) {
+                applicationPathType = PwmApplication.PwmEnvironment.ApplicationPathType.derived;
+                applicationPath = webInfPath;
+            } else {
+                applicationPath = new File(applicationPathStr);
+                applicationPathType = PwmApplication.PwmEnvironment.ApplicationPathType.specified;
+            }
+        }
+
         File configurationFile = null;
         try {
-            applicationPath = locateApplicationPath();
             configurationFile = locateConfigurationFile(applicationPath);
 
             configReader = new ConfigurationReader(configurationFile);
@@ -166,29 +180,43 @@ public class ContextManager implements Serializable {
                 outputError("Startup Error: " + (startupErrorInformation == null ? "un-specified error" : startupErrorInformation.toDebugStr()));
             }
         } catch (Throwable e) {
-            handleStartupError("unable to initialize application due to configuration related error: ",e);
+            handleStartupError("unable to initialize application due to configuration related error: ", e);
         }
+        LOGGER.debug("configuration file was loaded from " + (configurationFile == null ? "null" : configurationFile.getAbsoluteFile()));
+
 
         try {
-            pwmApplication = new PwmApplication(configuration, mode, applicationPath, true, configurationFile);
+            pwmApplication = new PwmApplication.PwmEnvironment()
+                    .setConfig(configuration)
+                    .setApplicationMode(mode)
+                    .setApplicationPath(applicationPath)
+                    .setInitLogging(true)
+                    .setConfigurationFile(configurationFile)
+                    .setWebInfPath(webInfPath)
+                    .setApplicationPathType(applicationPathType).createPwmApplication();
         } catch (Exception e) {
-            handleStartupError("unable to initialize application: ",e);
+            handleStartupError("unable to initialize application: ", e);
         }
 
         final String threadName = Helper.makeThreadName(pwmApplication, this.getClass()) + " timer";
         taskMaster = new Timer(threadName, true);
         taskMaster.schedule(new RestartFlagWatcher(), 1031, 1031);
 
-        final boolean reloadOnChange = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.CONFIG_RELOAD_ON_CHANGE));
-        if (reloadOnChange) {
-            final long fileScanFrequencyMs = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.CONFIG_FILE_SCAN_FREQUENCY));
-            taskMaster.schedule(new ConfigFileWatcher(), fileScanFrequencyMs, fileScanFrequencyMs);
+        boolean reloadOnChange = true;
+        long fileScanFrequencyMs = 5000;
+        {
+            if (pwmApplication != null) {
+                reloadOnChange = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.CONFIG_RELOAD_ON_CHANGE));
+                fileScanFrequencyMs = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.CONFIG_FILE_SCAN_FREQUENCY));
+            }
+            if (reloadOnChange) {
+                taskMaster.schedule(new ConfigFileWatcher(), fileScanFrequencyMs, fileScanFrequencyMs);
+            }
+
+            checkConfigForSaveOnRestart(configReader, pwmApplication);
         }
 
-        LOGGER.debug(
-                "configuration file was loaded from " + (configurationFile == null ? "null" : configurationFile.getAbsoluteFile()));
 
-        checkConfigForSaveOnRestart(configReader, pwmApplication);
     }
 
     private void checkConfigForSaveOnRestart(
@@ -223,19 +251,21 @@ public class ContextManager implements Serializable {
         final String errorMsg;
         if (throwable instanceof OutOfMemoryError) {
             errorMsg = "JAVA OUT OF MEMORY ERROR!, please allocate more memory for java: " + throwable.getMessage();
+            startupErrorInformation = new ErrorInformation(PwmError.ERROR_STARTUP_ERROR,errorMsg);
+        } else if (throwable instanceof PwmException) {
+            startupErrorInformation = ((PwmException)throwable).getErrorInformation().wrapWithNewErrorCode(PwmError.ERROR_STARTUP_ERROR);
         } else {
             errorMsg = throwable.getMessage();
+            startupErrorInformation = new ErrorInformation(PwmError.ERROR_APP_UNAVAILABLE, msgPrefix + errorMsg);
         }
 
-        startupErrorInformation = new ErrorInformation(PwmError.ERROR_APP_UNAVAILABLE, msgPrefix + errorMsg);
-
         try {
-            LOGGER.fatal(errorMsg);
+            LOGGER.fatal(startupErrorInformation.getDetailedErrorMsg());
         } catch (Exception e2) {
             // noop
         }
 
-        outputError(errorMsg);
+        outputError(startupErrorInformation.getDetailedErrorMsg());
         throwable.printStackTrace();
     }
 
@@ -348,7 +378,7 @@ public class ContextManager implements Serializable {
         return startupErrorInformation;
     }
 
-    private static interface EnvironmentTest {
+    private interface EnvironmentTest {
         ErrorInformation doTest();
     }
 
@@ -421,43 +451,61 @@ public class ContextManager implements Serializable {
         return new File(applicationPath.getAbsolutePath() + File.separator + configurationFileSetting);
     }
 
-    public File locateApplicationPath()
-            throws PwmException
-    {
-        if (this.servletContext == null) {
-            return null;
+    public File locateWebInfFilePath() {
+        final String realPath = servletContext.getRealPath("/WEB-INF");
+
+        if (realPath != null) {
+            final File servletPath = new File(realPath);
+            if (servletPath.exists()) {
+                return servletPath;
+            }
         }
 
-        // read system property.
+        return null;
+    }
+
+    public String readSpecifiedApplicationPath() {
+
         {
-            final String propertyName = PwmConstants.PWM_APP_NAME.toLowerCase() + ".sspr.applicationPath";
+            final String contextName = this.servletContext.getContextPath() == null
+                    ? PwmConstants.PWM_APP_NAME.toLowerCase()
+                    : this.servletContext.getContextPath().replaceAll("^/", ""); // strip leading slash
+
+            // java prop command name
+            final String propertyName = PwmConstants.PWM_APP_NAME.toLowerCase()
+                    + "."
+                    + contextName
+                    + "."
+                    + "applicationPath";
+
             final String propertyAppPath = System.getProperty(propertyName);
             if (propertyAppPath != null && !propertyAppPath.isEmpty()) {
-                return new File(propertyAppPath);
+                return propertyAppPath;
             }
         }
 
+        {
+            final String contextAppPathSetting = servletContext.getInitParameter(
+                    ContextParameter.applicationPath.toString());
 
-        final String contextAppPathSetting = servletContext.getInitParameter(
-                ContextParameter.applicationPath.toString());
-
-        // first try to check if context setting is a real directory.
-        try {
-            File file = new File(contextAppPathSetting);
-            if (file.exists() && file.isDirectory()) {
-                return file;
+            if (contextAppPathSetting != null && !contextAppPathSetting.isEmpty()) {
+                if (!"unspecified".equals(contextAppPathSetting)) {
+                    return contextAppPathSetting;
+                }
             }
-        } catch (Exception e) {
-            outputError("error testing context " + ContextParameter.applicationPath.toString() + " parameter to verify if it is a valid file path: " + e.getMessage());
+
+            return null;
         }
+    }
 
-        final String prefixedRealPath = contextAppPathSetting == null
-                ? "/" :
-                contextAppPathSetting.startsWith("/")
-                        ? contextAppPathSetting
-                        : "/" + contextAppPathSetting;
+    public File deriveApplicationPath()
+            throws PwmException
+    {
+        if (this.servletContext == null) {
+            return null;
+        }
 
-        final String realPath = servletContext.getRealPath(prefixedRealPath);
+        final String realPath = servletContext.getRealPath("/WEB-INF");
 
         if (realPath != null) {
             final File servletPath = new File(realPath);

+ 12 - 0
pwm/servlet/src/password/pwm/http/filter/RequestInitializationFilter.java

@@ -25,6 +25,8 @@ package password.pwm.http.filter;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.*;
 import password.pwm.util.logging.PwmLogger;
@@ -73,6 +75,16 @@ public class RequestInitializationFilter implements Filter {
         } catch (Throwable e) {
             LOGGER.error("can't load application: " + e.getMessage());
             if (!(new PwmURL(req).isResourceURL())) {
+                ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_APP_UNAVAILABLE);
+                try {
+                    ContextManager contextManager = ContextManager.getContextManager(servletRequest.getServletContext());
+                    if (contextManager != null) {
+                        errorInformation = contextManager.getStartupErrorInformation();
+                    }
+                } catch (Throwable e2) {
+                    e2.getMessage();
+                }
+                servletRequest.setAttribute(PwmConstants.REQUEST_ATTR.PwmErrorInfo.toString(),errorInformation);
                 final String url = PwmConstants.JSP_URL.APP_UNAVAILABLE.getPath();
                 servletRequest.getServletContext().getRequestDispatcher(url).forward(req, resp);
             }

+ 7 - 1
pwm/servlet/src/password/pwm/http/servlet/ConfigGuideServlet.java

@@ -336,7 +336,13 @@ public class ConfigGuideServlet extends PwmServlet {
     )
             throws IOException, PwmUnrecoverableException {
         final Configuration tempConfiguration = new Configuration(configGuideBean.getStoredConfiguration());
-        final PwmApplication tempApplication = new PwmApplication(tempConfiguration, PwmApplication.MODE.NEW, null, false, null);
+        final PwmApplication tempApplication = new PwmApplication.PwmEnvironment()
+                .setConfig(tempConfiguration)
+                .setApplicationMode(PwmApplication.MODE.NEW)
+                .setApplicationPath(null)
+                .setInitLogging(false)
+                .setConfigurationFile(null)
+                .setWebInfPath(null).createPwmApplication();
         final LDAPStatusChecker ldapStatusChecker = new LDAPStatusChecker();
         final List<HealthRecord> records = new ArrayList<>();
         final LdapProfile ldapProfile = tempConfiguration.getDefaultLdapProfile();

+ 1 - 1
pwm/servlet/src/password/pwm/http/servlet/ForgottenPasswordServlet.java

@@ -669,7 +669,7 @@ public class ForgottenPasswordServlet extends PwmServlet {
             theUser.unlockPassword();
 
             // mark the event log
-            pwmApplication.getAuditManager().submit(AuditEvent.UNLOCK_PASSWORD, pwmSession.getUserInfoBean(),pwmSession);
+            pwmApplication.getAuditManager().submit(AuditEvent.UNLOCK_PASSWORD, forgottenPasswordBean.getUserInfo(),pwmSession);
 
             pwmRequest.forwardToSuccessPage(Message.Success_UnlockAccount);
         } catch (ChaiOperationException e) {

+ 251 - 45
pwm/servlet/src/password/pwm/http/servlet/PeopleSearchServlet.java

@@ -24,6 +24,7 @@ package password.pwm.http.servlet;
 
 import com.google.gson.reflect.TypeToken;
 import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.exception.ChaiException;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
@@ -64,6 +65,54 @@ public class PeopleSearchServlet extends PwmServlet {
 
     private static final PwmLogger LOGGER = PwmLogger.forClass(PeopleSearchServlet.class);
 
+    public static class UserDetailBean implements Serializable {
+        private String displayName;
+        private String userKey;
+        private Map<String,AttributeDetailBean> detail;
+        private String photoURL;
+        private boolean hasOrgChart;
+
+        public String getDisplayName() {
+            return displayName;
+        }
+
+        public void setDisplayName(String displayName) {
+            this.displayName = displayName;
+        }
+
+        public String getUserKey() {
+            return userKey;
+        }
+
+        public void setUserKey(String userKey) {
+            this.userKey = userKey;
+        }
+
+        public Map<String, AttributeDetailBean> getDetail() {
+            return detail;
+        }
+
+        public void setDetail(Map<String, AttributeDetailBean> detail) {
+            this.detail = detail;
+        }
+
+        public String getPhotoURL() {
+            return photoURL;
+        }
+
+        public void setPhotoURL(String photoURL) {
+            this.photoURL = photoURL;
+        }
+
+        public boolean isHasOrgChart() {
+            return hasOrgChart;
+        }
+
+        public void setHasOrgChart(boolean hasOrgChart) {
+            this.hasOrgChart = hasOrgChart;
+        }
+    }
+
     public static class AttributeDetailBean implements Serializable {
         private String name;
         private String label;
@@ -119,11 +168,55 @@ public class PeopleSearchServlet extends PwmServlet {
         public void setSearchable(boolean searchable) {
             this.searchable = searchable;
         }
+
+
+    }
+
+    public static class UserTreeData implements Serializable {
+        private UserTreeReferenceBean parent;
+        private List<UserTreeReferenceBean> siblings;
+
+        public UserTreeReferenceBean getParent() {
+            return parent;
+        }
+
+        public void setParent(UserTreeReferenceBean parent) {
+            this.parent = parent;
+        }
+
+        public List<UserTreeReferenceBean> getSiblings() {
+            return siblings;
+        }
+
+        public void setSiblings(List<UserTreeReferenceBean> siblings) {
+            this.siblings = siblings;
+        }
+    }
+
+    public static class UserTreeReferenceBean extends UserReferenceBean {
+        public String photoURL;
+        public boolean hasMoreNodes;
+
+        public String getPhotoURL() {
+            return photoURL;
+        }
+
+        public void setPhotoURL(String photoURL) {
+            this.photoURL = photoURL;
+        }
+
+        public boolean isHasMoreNodes() {
+            return hasMoreNodes;
+        }
+
+        public void setHasMoreNodes(boolean hasMoreNodes) {
+            this.hasMoreNodes = hasMoreNodes;
+        }
     }
 
     public static class UserReferenceBean implements Serializable {
         private String userKey;
-        private String display;
+        private String displayName;
 
         public String getUserKey() {
             return userKey;
@@ -133,12 +226,12 @@ public class PeopleSearchServlet extends PwmServlet {
             this.userKey = userKey;
         }
 
-        public String getDisplay() {
-            return display;
+        public String getDisplayName() {
+            return displayName;
         }
 
-        public void setDisplay(String display) {
-            this.display = display;
+        public void setDisplayName(String displayName) {
+            this.displayName = displayName;
         }
     }
 
@@ -165,6 +258,7 @@ public class PeopleSearchServlet extends PwmServlet {
         detail(HttpMethod.POST),
         photo(HttpMethod.GET),
         clientData(HttpMethod.GET),
+        userTreeData(HttpMethod.POST),
 
         ;
 
@@ -230,6 +324,10 @@ public class PeopleSearchServlet extends PwmServlet {
                 case clientData:
                     restLoadClientData(pwmRequest);
                     return;
+
+                case userTreeData:
+                    restUserTreeData(pwmRequest);
+                    return;
             }
         }
 
@@ -254,9 +352,13 @@ public class PeopleSearchServlet extends PwmServlet {
                     formConfiguration.getLabel(pwmRequest.getLocale()));
         }
 
+        final boolean orgChartEnabled = orgChartIsEnabled(pwmRequest.getConfig());
+
         final HashMap<String,Object> returnValues = new HashMap<>();
         returnValues.put("peoplesearch_search_columns",searchColumns);
         returnValues.put("photo_style_attribute",photoStyle);
+        returnValues.put("peoplesearch_orgChart_enabled",orgChartEnabled);
+
         final RestResultBean restResultBean = new RestResultBean(returnValues);
         LOGGER.trace(pwmRequest, "returning clientData: " + JsonUtil.serialize(restResultBean));
         pwmRequest.outputJsonResult(restResultBean);
@@ -341,13 +443,11 @@ public class PeopleSearchServlet extends PwmServlet {
             return;
         }
 
-
-        final RestResultBean restResultBean = new RestResultBean();
         final LinkedHashMap<String, Object> outputData = new LinkedHashMap<>();
-        outputData.put("searchResults",
-                new ArrayList<>(results.resultsAsJsonOutput(pwmRequest.getPwmApplication())));
+        outputData.put("searchResults", new ArrayList<>(results.resultsAsJsonOutput(pwmRequest.getPwmApplication())));
         outputData.put("sizeExceeded", sizeExceeded);
-        restResultBean.setData(outputData);
+
+        final RestResultBean restResultBean = new RestResultBean(outputData);
         pwmRequest.outputJsonResult(restResultBean);
         final long maxCacheSeconds = pwmRequest.getConfig().readSettingAsLong(PwmSetting.PEOPLE_SEARCH_MAX_CACHE_SECONDS);
         if (maxCacheSeconds > 0) {
@@ -368,22 +468,94 @@ public class PeopleSearchServlet extends PwmServlet {
             final PwmSession pwmSession,
             final UserIdentity userIdentity
     )
-            throws PwmUnrecoverableException, ChaiUnavailableException {
+            throws PwmUnrecoverableException
+    {
         final Configuration config = pwmApplication.getConfig();
         final List<FormConfiguration> detailFormConfig = config.readSettingAsForm(PwmSetting.PEOPLE_SEARCH_DETAIL_FORM);
         final Map<String, String> attributeHeaderMap = UserSearchEngine.UserSearchResults.fromFormConfiguration(
                 detailFormConfig, pwmSession.getSessionStateBean().getLocale());
-        final ChaiUser theUser = useProxy(pwmApplication,pwmSession)
-                ? pwmApplication.getProxiedChaiUser(userIdentity)
-                : pwmSession.getSessionManager().getActor(pwmApplication, userIdentity);
-        Map<String, String> values = null;
+
+        if (orgChartIsEnabled(pwmApplication.getConfig())) {
+            final String orgChartParentAttr = config.readSettingAsString(PwmSetting.PEOPLE_SEARCH_ORGCHART_PARENT_ATTRIBUTE);
+            if (!attributeHeaderMap.containsKey(orgChartParentAttr)) {
+                attributeHeaderMap.put(orgChartParentAttr, orgChartParentAttr);
+            }
+            final String orgChartChildAttr = config.readSettingAsString(PwmSetting.PEOPLE_SEARCH_ORGCHART_CHILD_ATTRIBUTE);
+            if (!attributeHeaderMap.containsKey(orgChartChildAttr)) {
+                attributeHeaderMap.put(orgChartChildAttr, orgChartChildAttr);
+            }
+        }
+
         try {
-            values = theUser.readStringAttributes(attributeHeaderMap.keySet());
-        } catch (ChaiOperationException e) {
+            final ChaiUser theUser = useProxy(pwmApplication,pwmSession)
+                    ? pwmApplication.getProxiedChaiUser(userIdentity)
+                    : pwmSession.getSessionManager().getActor(pwmApplication, userIdentity);
+            final Map<String, String> values = theUser.readStringAttributes(attributeHeaderMap.keySet());
+            return new UserSearchEngine.UserSearchResults(attributeHeaderMap,
+                    Collections.singletonMap(userIdentity, values), false);
+        } catch (ChaiException e) {
             LOGGER.error("unexpected error during detail lookup of '" + userIdentity + "', error: " + e.getMessage());
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.forChaiError(e.getErrorCode()),e.getMessage()));
+        }
+    }
+
+    private void restUserTreeData(
+            final PwmRequest pwmRequest
+    )
+            throws IOException, PwmUnrecoverableException, ServletException
+    {
+        if (!orgChartIsEnabled(pwmRequest.getConfig())) {
+            throw new PwmUnrecoverableException(PwmError.ERROR_SERVICE_NOT_AVAILABLE);
+        }
+
+        final Map<String,String> requestInputMap = pwmRequest.readBodyAsJsonStringMap();
+        if (requestInputMap == null) {
+            return;
+        }
+        final String userKey = requestInputMap.get("userKey");
+        if (userKey == null || userKey.isEmpty()) {
+            return;
+        }
+        final boolean asParent = Boolean.parseBoolean(requestInputMap.get("asParent"));
+
+        final String parentAttribute = pwmRequest.getConfig().readSettingAsString(PwmSetting.PEOPLE_SEARCH_ORGCHART_PARENT_ATTRIBUTE);
+        final String childAttribute = pwmRequest.getConfig().readSettingAsString(PwmSetting.PEOPLE_SEARCH_ORGCHART_CHILD_ATTRIBUTE);
+        try {
+
+            UserDetailBean parentDetail = null;
+            if (asParent) {
+                parentDetail = makeUserDetailRequestImpl(pwmRequest, userKey);
+            } else {
+                final UserDetailBean selfDetailData = makeUserDetailRequestImpl(pwmRequest, userKey);
+                if (selfDetailData.getDetail().containsKey(parentAttribute)) {
+                    final UserReferenceBean parentReference = selfDetailData.getDetail().get(parentAttribute).getUserReferences().iterator().next();
+                    parentDetail = makeUserDetailRequestImpl(pwmRequest, parentReference.getUserKey());
+                }
+            }
+            final UserTreeData userTreeData = new UserTreeData();
+            if (parentDetail != null) {
+                final UserTreeReferenceBean managerTreeReference = userDetailToTreeReference(parentDetail, parentAttribute);
+                userTreeData.setParent(managerTreeReference);
+
+                if (parentDetail.getDetail() != null) {
+                    if (parentDetail.getDetail().containsKey(childAttribute)) {
+                        final List<UserTreeReferenceBean> siblings = new ArrayList<>();
+                        for (final UserReferenceBean siblingReferenceBean : parentDetail.getDetail().get(childAttribute).getUserReferences()) {
+                            final UserDetailBean siblingDetail = makeUserDetailRequestImpl(pwmRequest, siblingReferenceBean.getUserKey());
+                            final UserTreeReferenceBean siblingTreeReferenceBean = userDetailToTreeReference(siblingDetail, childAttribute);
+                            siblings.add(siblingTreeReferenceBean);
+                        }
+                        userTreeData.setSiblings(siblings);
+                    }
+                }
+            }
+            pwmRequest.outputJsonResult(new RestResultBean(userTreeData));
+        } catch (PwmOperationalException e) {
+            LOGGER.error(pwmRequest, "error generating user detail object: " + e.getMessage());
+            pwmRequest.respondWithError(e.getErrorInformation());
+        } catch (ChaiUnavailableException e) {
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.forChaiError(e.getErrorCode()),e.getMessage()));
         }
-        return new UserSearchEngine.UserSearchResults(attributeHeaderMap,
-                Collections.singletonMap(userIdentity, values), false);
     }
 
 
@@ -392,7 +564,6 @@ public class PeopleSearchServlet extends PwmServlet {
     )
             throws ChaiUnavailableException, PwmUnrecoverableException, IOException, ServletException
     {
-        final Date startTime = new Date();
         final Map<String, String> valueMap = pwmRequest.readBodyAsJsonStringMap();
 
         if (valueMap == null) {
@@ -404,6 +575,23 @@ public class PeopleSearchServlet extends PwmServlet {
             return;
         }
 
+        try {
+            final UserDetailBean detailData = makeUserDetailRequestImpl(pwmRequest, userKey);
+            pwmRequest.outputJsonResult(new RestResultBean(detailData));
+        } catch (PwmOperationalException e) {
+            LOGGER.error(pwmRequest, "error generating user detail object: " + e.getMessage());
+            pwmRequest.respondWithError(e.getErrorInformation());
+        }
+
+    }
+
+    private UserDetailBean makeUserDetailRequestImpl(
+            final PwmRequest pwmRequest,
+            final String userKey
+    )
+            throws PwmUnrecoverableException, IOException, ServletException, PwmOperationalException, ChaiUnavailableException
+    {
+        final Date startTime = new Date();
         final boolean useProxy = useProxy(pwmRequest.getPwmApplication(), pwmRequest.getPwmSession());
         final UserIdentity userIdentity = UserIdentity.fromKey(userKey, pwmRequest.getConfig());
 
@@ -415,12 +603,8 @@ public class PeopleSearchServlet extends PwmServlet {
         {
             final String cachedOutput = pwmRequest.getPwmApplication().getCacheService().get(cacheKey);
             if (cachedOutput != null) {
-                final HashMap<String, Object> resultOutput = JsonUtil.deserialize(cachedOutput,
-                        new TypeToken<HashMap<String, Object>>() {}
-                );
-                final RestResultBean restResultBean = new RestResultBean();
-                restResultBean.setData(new HashMap<>(resultOutput));
-                pwmRequest.outputJsonResult(restResultBean);
+                final UserDetailBean resultOutput = JsonUtil.deserialize(cachedOutput, UserDetailBean.class);
+                final RestResultBean restResultBean = new RestResultBean(resultOutput);
                 LOGGER.debug(pwmRequest.getPwmSession(), "finished rest detail request in " + TimeDuration.fromCurrent(
                         startTime).asCompactString() + " using cached details, results=" + JsonUtil.serialize(restResultBean));
 
@@ -428,52 +612,57 @@ public class PeopleSearchServlet extends PwmServlet {
                     pwmRequest.getPwmApplication().getStatisticsManager().incrementValue(Statistic.PEOPLESEARCH_DETAILS);
                 }
 
-                return;
+                return resultOutput;
             }
         }
+
         try {
             checkIfUserIdentityViewable(pwmRequest.getPwmApplication(), pwmRequest.getPwmSession(), userIdentity);
         } catch (PwmOperationalException e) {
             LOGGER.error(pwmRequest.getPwmSession(), "error during detail results request while checking if requested userIdentity is within search scope: " + e.getMessage());
-            pwmRequest.outputJsonResult(RestResultBean.fromError(e.getErrorInformation(), pwmRequest));
-            return;
+            throw e;
         }
 
         final UserSearchEngine.UserSearchResults detailResults = doDetailLookup(pwmRequest.getPwmApplication(), pwmRequest.getPwmSession(), userIdentity);
         final Map<String, String> searchResults = detailResults.getResults().get(userIdentity);
 
-        final LinkedHashMap<String, Object> resultOutput = new LinkedHashMap<>();
+        final UserDetailBean userDetailBean = new UserDetailBean();
+        userDetailBean.setUserKey(userKey);
         final List<FormConfiguration> detailFormConfig = pwmRequest.getConfig().readSettingAsForm( PwmSetting.PEOPLE_SEARCH_DETAIL_FORM);
-        List<AttributeDetailBean> bean = convertResultMapToBean(pwmRequest.getPwmApplication(), pwmRequest.getPwmSession(), userIdentity,
+        final Map<String,AttributeDetailBean> attributeBeans = convertResultMapToBeans(pwmRequest.getPwmApplication(), pwmRequest.getPwmSession(), userIdentity,
                 detailFormConfig, searchResults);
 
-        resultOutput.put("detail", bean);
+        userDetailBean.setDetail(attributeBeans);
         final String photoURL = figurePhotoURL(pwmRequest.getPwmApplication(), pwmRequest, userIdentity);
         if (photoURL != null) {
-            resultOutput.put("photoURL", photoURL);
+            userDetailBean.setPhotoURL(photoURL);
         }
         final String displayName = figureDisplaynameValue(pwmRequest.getPwmApplication(), pwmRequest.getPwmSession(), userIdentity);
         if (displayName != null) {
-            resultOutput.put("displayName", displayName);
+            userDetailBean.setDisplayName(displayName);
+        }
+
+        if (orgChartIsEnabled(pwmRequest.getConfig())) {
+            final String parentAttr = pwmRequest.getConfig().readSettingAsString(PwmSetting.PEOPLE_SEARCH_ORGCHART_PARENT_ATTRIBUTE);
+            if (searchResults.containsKey(parentAttr)) {
+                userDetailBean.setHasOrgChart(true);
+            }
         }
 
-        final RestResultBean restResultBean = new RestResultBean();
-        restResultBean.setData(resultOutput);
-        pwmRequest.outputJsonResult(restResultBean);
         LOGGER.debug(pwmRequest.getPwmSession(), "finished non-cached rest detail request in " + TimeDuration.fromCurrent(
-                startTime).asCompactString() + ", results=" + JsonUtil.serialize(restResultBean));
+                startTime).asCompactString() + ", results=" + JsonUtil.serialize(userDetailBean));
 
         final long maxCacheSeconds = pwmRequest.getConfig().readSettingAsLong(PwmSetting.PEOPLE_SEARCH_MAX_CACHE_SECONDS);
         if (maxCacheSeconds > 0) {
             final Date expiration = new Date(System.currentTimeMillis() * maxCacheSeconds * 1000);
             pwmRequest.getPwmApplication().getCacheService().put(cacheKey, CachePolicy.makePolicy(expiration),
-                    JsonUtil.serializeMap(resultOutput));
+                    JsonUtil.serialize(userDetailBean));
         }
 
         StatisticsManager.incrementStat(pwmRequest, Statistic.PEOPLESEARCH_SEARCHES);
+        return userDetailBean;
     }
 
-
     private static String figurePhotoURL(
             final PwmApplication pwmApplication,
             final PwmRequest pwmRequest,
@@ -561,7 +750,7 @@ public class PeopleSearchServlet extends PwmServlet {
         }
     }
 
-    private static List<AttributeDetailBean> convertResultMapToBean(
+    private static Map<String,AttributeDetailBean> convertResultMapToBeans(
             final PwmApplication pwmApplication,
             final PwmSession pwmSession,
             final UserIdentity userIdentity,
@@ -572,7 +761,7 @@ public class PeopleSearchServlet extends PwmServlet {
     {
         final int MAX_VALUES = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.PEOPLESEARCH_MAX_VALUE_COUNT));
         final Set<String> searchAttributes = getSearchAttributes(pwmApplication.getConfig());
-        final List<AttributeDetailBean> returnObj = new ArrayList<>();
+        final Map<String,AttributeDetailBean> returnObj = new LinkedHashMap<>();
         for (FormConfiguration formConfiguration : detailForm) {
             if (formConfiguration.isRequired() || searchResults.containsKey(formConfiguration.getName())) {
                 AttributeDetailBean bean = new AttributeDetailBean();
@@ -597,7 +786,7 @@ public class PeopleSearchServlet extends PwmServlet {
                                             loopIdentity);
                                     final UserReferenceBean userReference = new UserReferenceBean();
                                     userReference.setUserKey(loopIdentity.toObfuscatedKey(pwmApplication.getConfig()));
-                                    userReference.setDisplay(displayValue);
+                                    userReference.setDisplayName(displayValue);
                                     userReferences.put(displayValue, userReference);
                                 }
                             }
@@ -610,7 +799,7 @@ public class PeopleSearchServlet extends PwmServlet {
                     bean.setValue(searchResults.containsKey(formConfiguration.getName()) ? searchResults.get(
                             formConfiguration.getName()) : "");
                 }
-                returnObj.add(bean);
+                returnObj.put(formConfiguration.getName(),bean);
             }
         }
         return returnObj;
@@ -667,7 +856,7 @@ public class PeopleSearchServlet extends PwmServlet {
             final PwmSession pwmSession,
             final UserIdentity userIdentity
     )
-            throws ChaiUnavailableException, PwmUnrecoverableException, PwmOperationalException {
+            throws  PwmUnrecoverableException, PwmOperationalException {
         final String filterSetting = getSearchFilter(pwmApplication.getConfig());
         String filterString = filterSetting.replace(PwmConstants.VALUE_REPLACEMENT_USERNAME, "*");
         while (filterString.contains("**")) {
@@ -732,4 +921,21 @@ public class PeopleSearchServlet extends PwmServlet {
         final List<String> searchResultForm = configuration.readSettingAsStringArray(PwmSetting.PEOPLE_SEARCH_SEARCH_ATTRIBUTES);
         return Collections.unmodifiableSet(new HashSet<>(searchResultForm));
     }
+
+    private static UserTreeReferenceBean userDetailToTreeReference(final UserDetailBean userDetailBean, final String nextNodeAttribute) {
+        final UserTreeReferenceBean userTreeReferenceBean = new UserTreeReferenceBean();
+        userTreeReferenceBean.setUserKey(userDetailBean.getUserKey());
+        userTreeReferenceBean.setPhotoURL(userDetailBean.getPhotoURL());
+        userTreeReferenceBean.setDisplayName(userDetailBean.getDisplayName());
+        if (userDetailBean.getDetail() != null && userDetailBean.getDetail().containsKey(nextNodeAttribute)) {
+            userTreeReferenceBean.setHasMoreNodes(true);
+        }
+        return userTreeReferenceBean;
+    }
+
+    private static boolean orgChartIsEnabled(final Configuration config) {
+        final String orgChartParentAttr = config.readSettingAsString(PwmSetting.PEOPLE_SEARCH_ORGCHART_PARENT_ATTRIBUTE);
+        final String orgChartChildAttr = config.readSettingAsString(PwmSetting.PEOPLE_SEARCH_ORGCHART_CHILD_ATTRIBUTE);
+        return orgChartParentAttr != null && !orgChartParentAttr.isEmpty() && orgChartChildAttr != null && !orgChartChildAttr.isEmpty();
+    }
 }

+ 1 - 0
pwm/servlet/src/password/pwm/i18n/Display.properties

@@ -43,6 +43,7 @@ Button_Home=Home
 Button_Login=Login
 Button_Logout=Logout
 Button_More=More
+Button_OrgChart=Organizational Chart
 Button_RecoverPassword=Check Answers
 Button_Reset=Clear
 Button_Search=Search

+ 1 - 0
pwm/servlet/src/password/pwm/i18n/Error.properties

@@ -154,6 +154,7 @@ Error_SmsSendError=Unable to send sms message: %1%
 Error_LdapDataError=An LDAP data error has occurred.
 Error_MacroParseError=Macro parse error: %1%
 Error_NoProfileAssigned=No profile is assigned for this operation.
+Error_StartupError=An error occurred while starting the application.  Check the log files for information.
 
 Error_ConfigUploadSuccess=File uploaded successfully
 Error_ConfigUploadFailure=File failed to upload.

+ 11 - 0
pwm/servlet/src/password/pwm/util/cache/CacheService.java

@@ -60,6 +60,17 @@ public class CacheService implements PwmService {
             return;
         }
 
+        if (pwmApplication.getLocalDB() == null) {
+            LOGGER.debug("skipping cache service init due to localDB not being available");
+            status = STATUS.CLOSED;
+            return;
+        }
+
+        if (pwmApplication.getApplicationMode() == PwmApplication.MODE.READ_ONLY) {
+            LOGGER.debug("skipping cache service init due to read-only application mode");
+            status = STATUS.CLOSED;
+            return;
+        }
 
         status = STATUS.OPENING;
         final int maxMemItems = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.CACHE_MEMORY_MAX_ITEMS));

+ 21 - 6
pwm/servlet/src/password/pwm/util/cli/MainClass.java

@@ -32,6 +32,7 @@ import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
 import password.pwm.config.ConfigurationReader;
 import password.pwm.config.PwmSetting;
+import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.Helper;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBException;
@@ -50,6 +51,8 @@ public class MainClass {
 
     private static final String LOGGING_PATTERN = "%d{yyyy-MM-dd HH:mm:ss}, %-5p, %c{2}, %m%n";
 
+    private static MainOptions MAIN_OPTIONS = new MainOptions();
+
     public static final Map<String,CliCommand> COMMANDS;
     static {
         final List<CliCommand> commandList = new ArrayList<>();
@@ -71,6 +74,7 @@ public class MainClass {
         commandList.add(new VersionCommand());
         commandList.add(new LdapSchemaExtendCommand());
         commandList.add(new ConfigDeleteCommand());
+        commandList.add(new ResponseStatsCommand());
 
         final Map<String,CliCommand> sortedMap = new TreeMap<>();
         for (CliCommand command : commandList) {
@@ -207,8 +211,6 @@ public class MainClass {
         return returnObj;
     }
 
-    static MainOptions MAIN_OPTIONS = new MainOptions();
-
     public static void main(String[] args)
             throws Exception
     {
@@ -259,7 +261,7 @@ public class MainClass {
             if (arg != null) {
                 if (arg.startsWith(OPT_DEBUG_LEVEL)) {
                     if (arg.length() < OPT_DEBUG_LEVEL.length() + 2) {
-                        out(OPT_DEBUG_LEVEL + " switch must include level (example: -debugLevel=TRACE");
+                        out(OPT_DEBUG_LEVEL + " option must include level (example: -debugLevel=TRACE");
                         System.exit(-1);
                     } else {
                         final String levelStr = arg.substring(OPT_DEBUG_LEVEL.length() + 1, arg.length());
@@ -274,7 +276,7 @@ public class MainClass {
                     }
                 } else  if (arg.startsWith(OPT_APP_PATH)) {
                     if (arg.length() < OPT_DEBUG_LEVEL.length() + 2) {
-                        out(OPT_APP_PATH + " switch must include value (example: -debugLevel=/tmp/applicationPath");
+                        out(OPT_APP_PATH + " option must include value (example: -debugLevel=/tmp/applicationPath");
                         System.exit(-1);
                     } else {
                         final String pathStr = arg.substring(OPT_DEBUG_LEVEL.length() + 1, arg.length());
@@ -286,6 +288,7 @@ public class MainClass {
                             exitWithError(" specified applicationPath '" + pathStr + "' must be a directory");
                         }
                         MAIN_OPTIONS.applicationPath = pathValue;
+                        MAIN_OPTIONS.applicationPathType = PwmApplication.PwmEnvironment.ApplicationPathType.specified;
                     }
                 } else if (arg.equals(OPT_FORCE)) {
                     MAIN_OPTIONS.forceFlag = true;
@@ -343,10 +346,17 @@ public class MainClass {
     }
 
     static PwmApplication loadPwmApplication(final File applicationPath, final Configuration config, final File configurationFile, final boolean readonly)
-            throws LocalDBException
+            throws LocalDBException, PwmUnrecoverableException
     {
         final PwmApplication.MODE mode = readonly ? PwmApplication.MODE.READ_ONLY : PwmApplication.MODE.RUNNING;
-        final PwmApplication pwmApplication = new PwmApplication(config, mode, applicationPath, false, configurationFile);
+        final PwmApplication pwmApplication = new PwmApplication.PwmEnvironment()
+                .setConfig(config)
+                .setApplicationMode(mode)
+                .setApplicationPath(applicationPath)
+                .setApplicationPathType(MAIN_OPTIONS.applicationPathType)
+                .setInitLogging(false)
+                .setConfigurationFile(configurationFile)
+                .setWebInfPath(null).createPwmApplication();
         final PwmApplication.MODE runningMode = pwmApplication.getApplicationMode();
 
         if (runningMode != mode) {
@@ -368,6 +378,7 @@ public class MainClass {
     public static class MainOptions {
         private PwmLogLevel pwmLogLevel = null;
         private File applicationPath = null;
+        private PwmApplication.PwmEnvironment.ApplicationPathType applicationPathType = PwmApplication.PwmEnvironment.ApplicationPathType.derived;
         private boolean forceFlag = false;
 
         public PwmLogLevel getPwmLogLevel() {
@@ -381,6 +392,10 @@ public class MainClass {
         public boolean isForceFlag() {
             return forceFlag;
         }
+
+        public PwmApplication.PwmEnvironment.ApplicationPathType getApplicationPathType() {
+            return applicationPathType;
+        }
     }
 
     private static void exitWithError(final String msg) {

+ 157 - 0
pwm/servlet/src/password/pwm/util/cli/ResponseStatsCommand.java

@@ -0,0 +1,157 @@
+package password.pwm.util.cli;
+
+import com.novell.ldapchai.cr.Challenge;
+import com.novell.ldapchai.exception.ChaiOperationException;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.ResponseInfoBean;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.profile.LdapProfile;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.ldap.UserSearchEngine;
+import password.pwm.util.JsonUtil;
+import password.pwm.util.TimeDuration;
+import password.pwm.util.operations.CrService;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.Serializable;
+import java.util.*;
+
+public class ResponseStatsCommand extends AbstractCliCommand {
+
+    @Override
+    void doCommand()
+            throws Exception
+    {
+        final PwmApplication pwmApplication = cliEnvironment.getPwmApplication();
+        out("searching for users");
+        final List<UserIdentity> userIdentities = readAllUsersFromLdap(pwmApplication);
+        out("found " +  userIdentities.size() + " users, reading....");
+
+        final ResponseStats responseStats = makeStatistics(pwmApplication, userIdentities);
+
+        final File outputFile = (File)cliEnvironment.getOptions().get(CliParameters.REQUIRED_NEW_FILE.getName());
+        final long startTime = System.currentTimeMillis();
+        out("beginning output to " + outputFile.getAbsolutePath());
+        final FileOutputStream fileOutputStream = new FileOutputStream(outputFile,true);
+        fileOutputStream.write(JsonUtil.serialize(responseStats, JsonUtil.Flag.PrettyPrint).getBytes(PwmConstants.DEFAULT_CHARSET));
+        fileOutputStream.close();
+        out("completed writing stats output in " + TimeDuration.fromCurrent(startTime).asLongString());
+    }
+
+    static class ResponseStats implements Serializable {
+        private final Map<String,Integer> challengeTextOccurrence = new TreeMap<>();
+        private final Map<String,Integer> helpdeskChallengeTextOccurrence = new TreeMap<>();
+    }
+
+    static int userCounter = 0;
+    ResponseStats makeStatistics(
+            final PwmApplication pwmApplication,
+            final List<UserIdentity> userIdentities
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException
+    {
+        final ResponseStats responseStats = new ResponseStats();
+        final Timer timer = new Timer();
+        timer.scheduleAtFixedRate(new TimerTask() {
+            @Override
+            public void run() {
+                out("processing...  " + userCounter + " users read");
+            }
+        },30 * 1000, 30 * 1000);
+        final CrService crService = pwmApplication.getCrService();
+        for (final UserIdentity userIdentity : userIdentities) {
+            userCounter++;
+            final ResponseInfoBean responseInfoBean = crService.readUserResponseInfo(null, userIdentity, pwmApplication.getProxiedChaiUser(userIdentity));
+            makeStatistics(responseStats, responseInfoBean);
+        }
+        timer.cancel();
+        return responseStats;
+    }
+
+    static void makeStatistics(final ResponseStats responseStats, final ResponseInfoBean responseInfoBean) {
+        if (responseInfoBean != null) {
+            {
+                final Map<Challenge, String> crMap = responseInfoBean.getCrMap();
+                if (crMap != null) {
+                    for (final Challenge challenge : crMap.keySet()) {
+                        final String challengeText = challenge.getChallengeText();
+                        if (challengeText != null && !challengeText.isEmpty()) {
+                            if (!responseStats.challengeTextOccurrence.containsKey(challengeText)) {
+                                responseStats.challengeTextOccurrence.put(challengeText, 0);
+                            }
+                            responseStats.challengeTextOccurrence.put(challengeText,
+                                    1 + responseStats.challengeTextOccurrence.get(challengeText));
+                        }
+                    }
+                }
+            }
+            {
+                final Map<Challenge, String> helpdeskCrMap = responseInfoBean.getHelpdeskCrMap();
+                if (helpdeskCrMap != null) {
+                    for (final Challenge challenge : helpdeskCrMap.keySet()) {
+                        final String challengeText = challenge.getChallengeText();
+                        if (challengeText != null && !challengeText.isEmpty()) {
+                            if (!responseStats.helpdeskChallengeTextOccurrence.containsKey(challengeText)) {
+                                responseStats.helpdeskChallengeTextOccurrence.put(challengeText, 0);
+                            }
+                            responseStats.helpdeskChallengeTextOccurrence.put(challengeText,
+                                    1 + responseStats.helpdeskChallengeTextOccurrence.get(challengeText));
+                        }
+                    }
+                }
+            }
+        }
+
+    }
+
+    private static List<UserIdentity> readAllUsersFromLdap(
+            final PwmApplication pwmApplication
+    )
+            throws ChaiUnavailableException, ChaiOperationException, PwmUnrecoverableException, PwmOperationalException
+    {
+        final List<UserIdentity> returnList = new ArrayList<>();
+
+        for (final LdapProfile ldapProfile : pwmApplication.getConfig().getLdapProfiles().values()) {
+            final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication,null);
+            final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
+            searchConfiguration.setEnableValueEscaping(false);
+            searchConfiguration.setSearchTimeout(Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.REPORTING_LDAP_SEARCH_TIMEOUT)));
+
+            searchConfiguration.setUsername("*");
+            searchConfiguration.setEnableValueEscaping(false);
+            searchConfiguration.setFilter(ldapProfile.readSettingAsString(PwmSetting.LDAP_USERNAME_SEARCH_FILTER));
+            searchConfiguration.setLdapProfile(ldapProfile.getIdentifier());
+
+            final Map<UserIdentity,Map<String,String>> searchResults = userSearchEngine.performMultiUserSearch(
+                    searchConfiguration,
+                    Integer.MAX_VALUE,
+                    Collections.<String>emptyList()
+            );
+            returnList.addAll(searchResults.keySet());
+
+        }
+
+        return returnList;
+    }
+
+
+    @Override
+    public CliParameters getCliParameters()
+    {
+        CliParameters cliParameters = new CliParameters();
+        cliParameters.commandName = "ResponseStats";
+        cliParameters.description = "Various statistics about stored responses";
+        cliParameters.options = Collections.singletonList(CliParameters.REQUIRED_NEW_FILE);
+
+        cliParameters.needsPwmApplication = true;
+        cliParameters.readOnly = true;
+
+        return cliParameters;
+    }
+}

+ 24 - 13
pwm/servlet/src/password/pwm/util/report/ReportService.java

@@ -80,7 +80,7 @@ public class ReportService implements PwmService {
     }
 
     public void clear()
-            throws LocalDBException, PwmUnrecoverableException 
+            throws LocalDBException, PwmUnrecoverableException
     {
         final Date startTime = new Date();
         LOGGER.info(PwmConstants.REPORTING_SESSION_LABEL,"clearing cached report data");
@@ -174,7 +174,7 @@ public class ReportService implements PwmService {
     }
 
     private void initTempData()
-            throws LocalDBException, PwmUnrecoverableException 
+            throws LocalDBException, PwmUnrecoverableException
     {
         final String cleanFlag = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.REPORT_CLEAN_FLAG);
         if (!"true".equals(cleanFlag)) {
@@ -192,7 +192,7 @@ public class ReportService implements PwmService {
             LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,"error loading cached report status info into memory: " + e.getMessage());
         }
         reportStatus = reportStatus == null ? new ReportStatusInfo(settings.getSettingsHash()) : reportStatus; //safety
-        
+
         final String currentSettingCache = settings.getSettingsHash();
         if (reportStatus.getSettingsHash() != null && !reportStatus.getSettingsHash().equals(currentSettingCache)) {
             LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,"configuration has changed, will clear cached report data");
@@ -201,7 +201,7 @@ public class ReportService implements PwmService {
 
         pwmApplication.writeAppAttribute(PwmApplication.AppAttribute.REPORT_CLEAN_FLAG, "false");
     }
-    
+
     @Override
     public List<HealthRecord> healthCheck()
     {
@@ -236,7 +236,7 @@ public class ReportService implements PwmService {
         reportStatus.setInProgress(true);
         reportStatus.setStartDate(new Date());
         try {
-            final Queue<UserIdentity> allUsers = new LinkedList<>(generateListOfUsers());
+            final Queue<UserIdentity> allUsers = new LinkedList<>(getListOfUsers());
             reportStatus.setTotal(allUsers.size());
             while (status == STATUS.OPEN && !allUsers.isEmpty() && !cancelFlag) {
                 final long startUpdateTime = System.currentTimeMillis();
@@ -385,22 +385,33 @@ public class ReportService implements PwmService {
         return reportStatus;
     }
 
-    private List<UserIdentity> generateListOfUsers()
+    private List<UserIdentity> getListOfUsers()
+            throws ChaiUnavailableException, ChaiOperationException, PwmUnrecoverableException, PwmOperationalException
+    {
+        return readAllUsersFromLdap(pwmApplication, settings.getSearchFilter(), settings.getMaxSearchSize());
+    }
+
+    private static List<UserIdentity> readAllUsersFromLdap(
+            final PwmApplication pwmApplication,
+            final String searchFilter,
+            final int maxResults
+    )
             throws ChaiUnavailableException, ChaiOperationException, PwmUnrecoverableException, PwmOperationalException
     {
         final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication,null);
         final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
         searchConfiguration.setEnableValueEscaping(false);
         searchConfiguration.setSearchTimeout(Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.REPORTING_LDAP_SEARCH_TIMEOUT)));
-        if (settings.getSearchFilter() == null) {
+
+        if (searchFilter == null) {
             searchConfiguration.setUsername("*");
         } else {
-            searchConfiguration.setFilter(settings.getSearchFilter());
+            searchConfiguration.setFilter(searchFilter);
         }
 
         LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"beginning UserReportService user search using parameters: " + (JsonUtil.serialize(searchConfiguration)));
 
-        final Map<UserIdentity,Map<String,String>> searchResults = userSearchEngine.performMultiUserSearch(searchConfiguration, settings.getMaxSearchSize(), Collections.<String>emptyList());
+        final Map<UserIdentity,Map<String,String>> searchResults = userSearchEngine.performMultiUserSearch(searchConfiguration, maxResults, Collections.<String>emptyList());
         LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"user search found " + searchResults.size() + " users for reporting");
         final List<UserIdentity> returnList = new ArrayList<>(searchResults.keySet());
         Collections.shuffle(returnList);
@@ -458,9 +469,9 @@ public class ReportService implements PwmService {
             storageKeyIterator.close();
         }
     }
-    
-    public void outputSummaryToCsv(final OutputStream outputStream, final Locale locale) 
-            throws IOException 
+
+    public void outputSummaryToCsv(final OutputStream outputStream, final Locale locale)
+            throws IOException
     {
         final List<ReportSummaryData.PresentationRow> outputList = summaryData.asPresentableCollection(pwmApplication.getConfig(),locale);
         final CSVPrinter csvPrinter = Helper.makeCsvPrinter(outputStream);
@@ -472,7 +483,7 @@ public class ReportService implements PwmService {
             headerRow.add(presentationRow.getPct());
             csvPrinter.printRecord(headerRow);
         }
-        
+
         csvPrinter.close();
     }
 

+ 1 - 1
pwm/servlet/src/password/pwm/wordlist/SeedlistManager.java

@@ -90,7 +90,7 @@ public class SeedlistManager extends AbstractWordlist implements Wordlist {
     public void init(final PwmApplication pwmApplication) throws PwmException {
         super.init(pwmApplication);
         final String setting = pwmApplication.getConfig().readSettingAsString(PwmSetting.SEEDLIST_FILENAME);
-        final File seedlistFile = setting == null || setting.length() < 1 ? null : Helper.figureFilepath(setting, pwmApplication.getApplicationPath());
+        final File seedlistFile = setting == null || setting.length() < 1 ? null : Helper.figureFilepath(setting, pwmApplication.getWebInfPath());
         final int loadFactor = PwmConstants.DEFAULT_WORDLIST_LOADFACTOR;
         final WordlistConfiguration wordlistConfiguration = new WordlistConfiguration(seedlistFile, loadFactor, true, 0);
 

+ 1 - 1
pwm/servlet/src/password/pwm/wordlist/WordlistManager.java

@@ -66,7 +66,7 @@ public class WordlistManager extends AbstractWordlist implements Wordlist {
     public void init(final PwmApplication pwmApplication) throws PwmException {
         super.init(pwmApplication);
         final String setting = pwmApplication.getConfig().readSettingAsString(PwmSetting.WORDLIST_FILENAME);
-        final File wordlistFile = setting == null || setting.length() < 1 ? null : Helper.figureFilepath(setting, pwmApplication.getApplicationPath());
+        final File wordlistFile = setting == null || setting.length() < 1 ? null : Helper.figureFilepath(setting, pwmApplication.getWebInfPath());
         final boolean caseSensitive = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.WORDLIST_CASE_SENSITIVE);
         final int loadFactor = PwmConstants.DEFAULT_WORDLIST_LOADFACTOR;
         final int checkSize = (int)pwmApplication.getConfig().readSettingAsLong(PwmSetting.PASSWORD_WORDLIST_WORDSIZE);

+ 11 - 3
pwm/servlet/web/WEB-INF/jsp/application-unavailable.jsp

@@ -1,4 +1,5 @@
 <%@ page import="password.pwm.PwmConstants" %>
+<%@ page import="password.pwm.error.ErrorInformation" %>
 <%@ page import="password.pwm.error.PwmError" %>
 <%--
   ~ Password Management Servlets (PWM)
@@ -23,8 +24,10 @@
   --%>
 
 <!DOCTYPE html>
-<%@ page language="java" session="true" isThreadSafe="true"
-         contentType="text/html; charset=UTF-8" %>
+<%@ page language="java" session="true" isThreadSafe="true" contentType="text/html; charset=UTF-8" %>
+<%
+    final ErrorInformation startupError = (ErrorInformation)request.getAttribute(PwmConstants.REQUEST_ATTR.PwmErrorInfo.toString());
+%>
 <html>
 <head>
     <title><%=PwmConstants.PWM_APP_NAME%></title>
@@ -46,7 +49,12 @@
         <h2><%=PwmError.ERROR_APP_UNAVAILABLE.toInfo().toDebugStr()%></h2>
         <br/>
         <br/>
-        <p><%=PwmError.ERROR_APP_UNAVAILABLE.toInfo().toUserStr(request.getLocale(),null)%></p>
+        <p><%=PwmError.ERROR_APP_UNAVAILABLE.toInfo().toUserStr(request.getLocale(), null)%></p>
+        <% if (startupError != null) { %>
+        <br/>
+        <br/>
+        <p><%=startupError.toDebugStr()%></p>
+        <% } %>
     </div>
     <div class="push"></div>
 </div>

+ 1 - 1
pwm/servlet/web/WEB-INF/jsp/error-http.jsp

@@ -31,7 +31,7 @@
 <% JspUtility.setFlag(pageContext, PwmRequest.Flag.NO_REQ_COUNTER); %>
 <%@ include file="fragment/header.jsp" %>
 <% final int statusCode = pageContext.getErrorData().getStatusCode(); %>
-<body class="nihilo">
+<body class="nihilo" data-jsp-page="error-http.jsp">
 <div id="wrapper">
     <jsp:include page="fragment/header-body.jsp">
         <jsp:param name="pwm.PageName" value="Title_Error"/>

+ 1 - 10
pwm/servlet/web/WEB-INF/jsp/error.jsp

@@ -1,5 +1,4 @@
 <%@ page import="password.pwm.error.ErrorInformation" %>
-<%@ page import="password.pwm.error.PwmException" %>
 <%@ page import="password.pwm.http.JspUtility" %>
 <%--
   ~ Password Management Servlets (PWM)
@@ -27,21 +26,13 @@
 <%@ page language="java" session="true" isThreadSafe="true"
          contentType="text/html; charset=UTF-8" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
-<%
-    PwmRequest pwmRequest = null;
-    try {
-        pwmRequest = PwmRequest.forRequest(request,response);
-    } catch (PwmException e) {
-        /* noop */
-    }
-%>
 <% final ErrorInformation errorInformation = (ErrorInformation)JspUtility.getAttribute(pageContext, PwmConstants.REQUEST_ATTR.PwmErrorInfo); %>
 <html dir="<pwm:LocaleOrientation/>">
 <% JspUtility.setFlag(pageContext, PwmRequest.Flag.HIDE_HEADER_BUTTONS); %>
 <% JspUtility.setFlag(pageContext, PwmRequest.Flag.HIDE_HEADER_WARNINGS); %>
 <% JspUtility.setFlag(pageContext, PwmRequest.Flag.NO_REQ_COUNTER); %>
 <%@ include file="fragment/header.jsp" %>
-<body class="nihilo">
+<body class="nihilo" data-jsp-page="error.jsp">
 <div id="wrapper">
     <jsp:include page="fragment/header-body.jsp">
         <jsp:param name="pwm.PageName" value="Title_Error"/>

+ 2 - 2
pwm/servlet/web/WEB-INF/web.xml

@@ -31,10 +31,10 @@
     <context-param>
         <description>
             Explicit location of application working directory. If a relative path is specified, it is relative to the
-            deployed applications base directory.
+            deployed applications base directory.  If the value is "unspecified", the /WEB-INF directory is assumed.
         </description>
         <param-name>applicationPath</param-name>
-        <param-value>/WEB-INF</param-value>
+        <param-value>unspecified</param-value>
     </context-param>
     <context-param>
         <description>

+ 22 - 19
pwm/servlet/web/public/resources/js/configeditor-settings.js

@@ -563,7 +563,6 @@ FormTableHandler.init = function(keyName) {
 FormTableHandler.redraw = function(keyName) {
     var resultValue = PWM_VAR['clientSettingCache'][keyName];
     var parentDiv = 'table_setting_' + keyName;
-    PWM_CFGEDIT.clearDivElements(parentDiv, false);
     var parentDivElement = PWM_MAIN.getObject(parentDiv);
 
     if (!PWM_MAIN.isEmpty(resultValue)) {
@@ -599,7 +598,7 @@ FormTableHandler.drawRow = function(parentDiv, settingKey, iteration, value) {
         newTableRow.setAttribute("style", "border-width: 0");
 
         var htmlRow = '';
-        htmlRow += '<td><input style="width:180px" class="configStringInput" id="' + inputID + 'name" value="' + value['name'] + '"/></td>';
+        htmlRow += '<td style="width:180px" id="panel-name-' + inputID + '" class="noWrapTextBox"></td>';
         htmlRow += '<td style="width:170px"><div class="noWrapTextBox" id="' + inputID + 'label"><span class="btn-icon fa fa-edit"></span><span>' + value['labels'][''] + '...</span></div></td>';
 
         htmlRow += '<td>';
@@ -643,6 +642,8 @@ FormTableHandler.drawRow = function(parentDiv, settingKey, iteration, value) {
         var parentDivElement = PWM_MAIN.getObject(parentDiv);
         parentDivElement.appendChild(newTableRow);
 
+        UILibrary.addTextValueToElement("panel-name-" + inputID,value['name']);
+
         PWM_MAIN.addEventHandler(inputID + "-moveUp", 'click', function () {
             FormTableHandler.move(settingKey, true, iteration);
         });
@@ -706,24 +707,26 @@ FormTableHandler.arrayMoveUtil = function(arr, fromIndex, toIndex) {
 
 
 FormTableHandler.addRow = function(keyName) {
-    var body='Name <input class="configStringInput" id="newFormFieldName" style="width:300px"/>';
-    PWM_MAIN.showConfirmDialog({title:'New Form Field',text:body,showClose:true,loadFunction:function(){
-        PWM_MAIN.getObject('dialog_ok_button').disabled = true;
-        PWM_MAIN.addEventHandler('newFormFieldName','input',function(){
-            PWM_VAR['newFormFieldName'] = PWM_MAIN.getObject('newFormFieldName').value;
-            if (PWM_VAR['newFormFieldName'] && PWM_VAR['newFormFieldName'].length > 1) {
-                PWM_MAIN.getObject('dialog_ok_button').disabled = false;
+    UILibrary.stringEditorDialog({
+        title:PWM_SETTINGS['settings'][keyName]['label'] + ' - New Form Field',
+        regex:'^[a-zA-Z][a-zA-Z0-9-]*$',
+        placeholder:'FieldName',
+        completeFunction:function(value){
+            for (var i in PWM_VAR['clientSettingCache'][keyName]) {
+                if (PWM_VAR['clientSettingCache'][keyName][i]['name'] == value) {
+                    alert('field already exists');
+                    return;
+                }
             }
-        });
-    },okAction:function(){
-        var currentSize = PWM_MAIN.itemCount(PWM_VAR['clientSettingCache'][keyName]);
-        PWM_VAR['clientSettingCache'][keyName][currentSize + 1] = FormTableHandler.newRowValue;
-        PWM_VAR['clientSettingCache'][keyName][currentSize + 1].name = PWM_VAR['newFormFieldName'];
-        PWM_VAR['clientSettingCache'][keyName][currentSize + 1].labels = {'':PWM_VAR['newFormFieldName']};
-        FormTableHandler.writeFormSetting(keyName,function(){
-            FormTableHandler.init(keyName);
-        });
-    }});
+            var currentSize = PWM_MAIN.itemCount(PWM_VAR['clientSettingCache'][keyName]);
+            PWM_VAR['clientSettingCache'][keyName][currentSize + 1] = FormTableHandler.newRowValue;
+            PWM_VAR['clientSettingCache'][keyName][currentSize + 1].name = value;
+            PWM_VAR['clientSettingCache'][keyName][currentSize + 1].labels = {'':value};
+            FormTableHandler.writeFormSetting(keyName,function(){
+                FormTableHandler.init(keyName);
+            });
+        }
+    });
 };
 
 FormTableHandler.showOptionsDialog = function(keyName, iteration) {

+ 15 - 3
pwm/servlet/web/public/resources/js/main.js

@@ -103,7 +103,7 @@ PWM_MAIN.loadLocaleBundle = function(bundleName, completeFunction) {
         } else {
             PWM_GLOBAL['localeStrings'] = PWM_GLOBAL['localeStrings'] || {};
             PWM_GLOBAL['localeStrings'][bundleName] = {};
-            for (var settingKey in data['data']) {
+                for (var settingKey in data['data']) {
                 PWM_GLOBAL['localeStrings'][bundleName][settingKey] = data['data'][settingKey];
             }
         }
@@ -128,6 +128,16 @@ PWM_MAIN.initPage = function() {
         console.log('error during autofocus support extension: ' + e);
     }
 
+    require(["dojo"], function (dojo) {
+        if (dojo.isIE) {
+            document.body.setAttribute('data-browserType','ie');
+        } else if (dojo.isFF) {
+            document.body.setAttribute('data-browserType','ff');
+        } else if (dojo.isWebKit) {
+            document.body.setAttribute('data-browserType','webkit');
+        }
+    });
+
     require(["dojo", "dojo/on"], function (dojo, on) {
         on(document, "keypress", function (event) {
             PWM_MAIN.checkForCapsLock(event);
@@ -394,10 +404,12 @@ PWM_MAIN.handleLoginFormSubmit = function(form, event) {
             var loadFunction = function(data) {
 
                 if (data['error'] == true) {
+                    PWM_MAIN.getObject('password').value = '';
                     PWM_MAIN.showErrorDialog(data,{
                         okAction:function(){
-                            PWM_MAIN.getObject('password').value = '';
-                            PWM_MAIN.getObject('password').focus();
+                            setTimeout(function(){
+                                PWM_MAIN.getObject('password').focus();
+                            },50);
                         }
                     });
                     return;

+ 155 - 26
pwm/servlet/web/public/resources/js/peoplesearch.js

@@ -51,17 +51,8 @@ PWM_PS.processPeopleSearch = function() {
             var sizeExceeded = data['data']['sizeExceeded'];
             grid.refresh();
             grid.renderArray(gridData);
-            grid.on(".dgrid-row:click", function(evt){
-                if (PWM_VAR['detailInProgress'] != true) {
-                    PWM_VAR['detailInProgress'] = true;
-                    evt.preventDefault();
-                    var row = grid.row(evt);
-                    var userKey = row.data['userKey'];
-                    PWM_PS.showUserDetail(userKey);
-                } else {
-                    console.log('ignoring dupe detail request event');
-                }
-            });
+            grid.set("sort", { attribute : 'givenName'});
+
 
             if (sizeExceeded) {
                 PWM_MAIN.getObject('maxResultsIndicator').style.display = 'inherit';
@@ -109,10 +100,11 @@ PWM_PS.convertDetailResultToHtml = function(data) {
                     (function(refIterInner){
                         var reference = userReferences[refIterInner];
                         var userKey = reference['userKey'];
-                        var displayValue = reference['display'];
+                        var displayValue = reference['displayName'];
                         htmlBody += '<a id="link-' + userKey + '-' + reference + '">';
                         htmlBody += displayValue;
-                        htmlBody += "</a><br/>";
+                        htmlBody += "</a>";
+                        htmlBody += "<br/>";
                     })(refIter);
                 }
                 htmlBody += '</div>';
@@ -194,10 +186,23 @@ PWM_PS.showUserDetail = function(userKey) {
                         if (photoURL) {
                             PWM_PS.loadPicture(PWM_MAIN.getObject("userPhotoParentDiv"),photoURL);
                         }
+
                         PWM_PS.applyEventHandlersToDetailView(data['data']);
+
+                        if (PWM_VAR['peoplesearch_orgChart_enabled'] && data['data']['hasOrgChart']) {
+                            var buttonObj = document.createElement('button');
+                            buttonObj.setAttribute('class', 'btn');
+                            buttonObj.id = 'button-orgChart';
+                            buttonObj.innerHTML = '<span class="btn-icon fa fa-sitemap"></span> ' + PWM_MAIN.showString('Button_OrgChart');
+                            PWM_MAIN.getObject('dialog_ok_button').parentElement.appendChild(buttonObj);
+                        }
+
                         setTimeout(function() {
                             try {PWM_MAIN.getObject('dialog_ok_button').focus(); } catch (e) { /*noop */}
                         },1000);
+                        PWM_MAIN.addEventHandler('button-orgChart','click',function(){
+                            PWM_PS.showOrgChartView(userKey);
+                        });
                     }
                 });
             };
@@ -206,25 +211,147 @@ PWM_PS.showUserDetail = function(userKey) {
     });
 };
 
+PWM_PS.convertUserTreeDataToOrgChartHtml = function(data) {
+    var htmlOutput = '<div>';
+    if ('parent' in data) {
+        var parentReference = data['parent'];
+        htmlOutput += '<div class="panel-orgChart-parent">';
+        if (parentReference['hasMoreNodes']) {
+            htmlOutput += '<a id="link-parent-' + parentReference['userKey'] + '"><span class="fa fa-arrow-up"/> </a>';
+        }
+        htmlOutput += '<div class="panel-orgChart-person">';
+        if ('photoURL' in parentReference) {
+            htmlOutput += '<img class="img-orgChart" id="" src="' + parentReference['photoURL'] + '">';
+        }
+        htmlOutput += '<div class="panel-orgChart-displayName">' + parentReference['displayName'] + '</div>';
+        htmlOutput += ' <span id="button-userDetail-' + parentReference['userKey'] + '" class="btn-icon fa fa-info-circle">';
+        htmlOutput += '</div></div><br/>';
+    }
+    if ('siblings' in data) {
+        for (var iter in data['siblings']) {
+            (function(iterCount){
+                var siblingReference = data['siblings'][iterCount];
+                htmlOutput += '<div class="panel-orgChart-child">';
+                if (siblingReference['hasMoreNodes']) {
+                    htmlOutput += '<a id="link-sibling-' + siblingReference['userKey'] + '"><span class="fa fa-arrow-down"/> </a>';
+                }
+                htmlOutput += '<div class="panel-orgChart-person">';
+                if ('photoURL' in siblingReference) {
+                    htmlOutput += '<img class="img-orgChart" id="img-orgChart-' + siblingReference['userKey'] + '" src="' + siblingReference['photoURL'] + '">';
+                }
+
+                htmlOutput += '<div class="panel-orgChart-displayName">' + siblingReference['displayName'] + '</div>';
+                htmlOutput += ' <span id="button-userDetail-' + siblingReference['userKey'] + '" class="btn-icon fa fa-info-circle">';
+                htmlOutput += '</div></div><br/>';
+            })(iter);
+        }
+    }
+    htmlOutput += '</div>';
+    return htmlOutput;
+};
+
+PWM_PS.applyUserTreeDataToOrgChartEvents = function(data) {
+    if ('parent' in data) {
+        var parentReference = data['parent'];
+        if (parentReference['hasMoreNodes']) {
+            PWM_MAIN.addEventHandler('link-parent-' + parentReference['userKey'], 'click', function () {
+                PWM_PS.showOrgChartView(parentReference['userKey'])
+            });
+        }
+        PWM_MAIN.addEventHandler('button-userDetail-' + parentReference['userKey'],'click',function(){
+            PWM_PS.showUserDetail(parentReference['userKey']);
+        });
+    }
+    if ('siblings' in data) {
+        for (var iter in data['siblings']) {
+            (function(iterCount){
+                var siblingReference = data['siblings'][iterCount];
+                if (siblingReference['hasMoreNodes']) {
+                    PWM_MAIN.addEventHandler('link-sibling-' + siblingReference['userKey'], 'click', function () {
+                        PWM_PS.showOrgChartView(siblingReference['userKey'], true)
+                    });
+                }
+                PWM_MAIN.addEventHandler('button-userDetail-' + siblingReference['userKey'],'click',function(){
+                    PWM_PS.showUserDetail(siblingReference['userKey']);
+                });
+            })(iter);
+        }
+    }
+};
+
+
+PWM_PS.showOrgChartView = function(userKey, asParent) {
+    console.log('beginning showOrgChartView, userKey=' + userKey);
+    var sendData = {
+        userKey:userKey,
+        asParent:asParent
+    };
+    PWM_MAIN.showWaitDialog({
+        loadFunction:function(){
+            var url = "PeopleSearch?processAction=userTreeData";
+            var loadFunction = function(data) {
+                if (data['error'] == true) {
+                    PWM_MAIN.closeWaitDialog();
+                    PWM_MAIN.showErrorDialog(data);
+                    return;
+                }
+                var htmlBody = PWM_PS.convertUserTreeDataToOrgChartHtml(data['data']);
+                PWM_MAIN.closeWaitDialog();
+                PWM_MAIN.showDialog({
+                    title:PWM_MAIN.showString('Button_OrgChart'),
+                    allowMove:true,
+                    text:htmlBody,
+                    showClose:true,
+                    loadFunction:function(){
+                        PWM_PS.applyUserTreeDataToOrgChartEvents(data['data']);
+                        setTimeout(function() {
+                            try {PWM_MAIN.getObject('dialog_ok_button').focus(); } catch (e) { /*noop */}
+                        },1000);
+                    }
+                });
+            };
+            PWM_MAIN.ajaxRequest(url, loadFunction, {content:sendData});
+        }
+    });
+
+};
+
 PWM_PS.makeSearchGrid = function(nextFunction) {
-        require(["dojo","dojo/_base/declare", "dgrid/Grid", "dgrid/Keyboard", "dgrid/Selection", "dgrid/extensions/ColumnResizer", "dgrid/extensions/ColumnReorder", "dgrid/extensions/ColumnHider", "dojo/domReady!"],
-            function(dojo,declare, Grid, Keyboard, Selection, ColumnResizer, ColumnReorder, ColumnHider){
-                PWM_MAIN.getObject('peoplesearch-searchResultsGrid').innerHTML = '';
+    require(["dojo","dojo/_base/declare", "dgrid/Grid", "dgrid/Keyboard", "dgrid/Selection", "dgrid/extensions/ColumnResizer", "dgrid/extensions/ColumnReorder", "dgrid/extensions/ColumnHider", "dojo/domReady!"],
+        function(dojo,declare, Grid, Keyboard, Selection, ColumnResizer, ColumnReorder, ColumnHider){
+            PWM_MAIN.getObject('peoplesearch-searchResultsGrid').innerHTML = '';
 
-                var CustomGrid = declare([ Grid, Keyboard, Selection, ColumnResizer, ColumnReorder, ColumnHider ]);
+            var CustomGrid = declare([ Grid, Keyboard, Selection, ColumnResizer, ColumnReorder, ColumnHider ]);
 
-                PWM_VAR['peoplesearch_search_grid'] = new CustomGrid({
-                    columns: PWM_VAR['peoplesearch_search_columns']
-                }, "peoplesearch-searchResultsGrid");
+            PWM_VAR['peoplesearch_search_grid'] = new CustomGrid({
+                columns: PWM_VAR['peoplesearch_search_columns'],
+                queryOptions: {
+                    sort: [{ attribute: "sn" }]
+                }
+            }, "peoplesearch-searchResultsGrid");
 
-                if (nextFunction) {
-                    nextFunction();
+            if (nextFunction) {
+                nextFunction();
+            }
+
+            PWM_VAR['peoplesearch_search_grid'].on(".dgrid-row:click", function(evt){
+                if (PWM_VAR['detailInProgress'] != true) {
+                    PWM_VAR['detailInProgress'] = true;
+                    evt.preventDefault();
+                    var row = PWM_VAR['peoplesearch_search_grid'].row(evt);
+                    var userKey = row.data['userKey'];
+                    PWM_PS.showUserDetail(userKey);
+                } else {
+                    console.log('ignoring dupe detail request event');
                 }
             });
+
+        }
+    );
 };
 
 PWM_PS.loadPicture = function(parentDiv,url) {
-    if (url.lastIndexOf('http', 0) !== 0) {
+    if (url.lastIndexOf('http', 0) !== 0) { // if not absolute url
         url = PWM_MAIN.addPwmFormIDtoURL(url);
     }
     require(["dojo/on"], function(on){
@@ -232,10 +359,12 @@ PWM_PS.loadPicture = function(parentDiv,url) {
         image.setAttribute('id',"userPhotoImage");
         image.setAttribute('style',PWM_VAR['photo_style_attribute']);
         on(image,"load",function(){
-            while (parentDiv.firstChild) {
-                parentDiv.removeChild(parentDiv.firstChild);
+            if (parentDiv) {
+                while (parentDiv.firstChild) {
+                    parentDiv.removeChild(parentDiv.firstChild);
+                }
+                parentDiv.appendChild(image);
             }
-            parentDiv.appendChild(image);
         });
         image.src = url;
     });

+ 23 - 1
pwm/servlet/web/public/resources/style.css

@@ -540,7 +540,7 @@ img.qrcodeimage {
 
 #peoplesearch-searchResultsGrid {
     min-height: 400px;
-    height: 70vh;
+    height: 60vh;
 }
 
 #helpdesk-searchResultsGrid {
@@ -692,3 +692,25 @@ progress:not([value]) {
 .dgrid-row-odd {
     background: #fdfdfd;
 }
+
+.panel-orgChart-person {
+    margin:0;
+    display: inline-block;
+    background-color: #D4D4D4;
+    padding: 3px;
+    border-radius: 3px;
+    background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #D4D4D4), color-stop(1, #EEEEEE) );
+    background:linear-gradient(to bottom, #D4D4D4 5%, #EEEEEE 100% );
+    margin-bottom: 5px;
+    width:auto;
+}
+
+.panel-orgChart-child {
+    display: inline-block;
+    margin-left: 25px;
+}
+
+.img-orgChart {
+    height:25px;
+    width:25px
+}