فهرست منبع

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/main/java/password/pwm/config/PwmSettingSyntax.java
#	src/main/java/password/pwm/config/value/data/FormConfiguration.java
#	src/main/webapp/public/resources/js/configeditor-settings.js
Jason Rivard 8 سال پیش
والد
کامیت
45ec8c4b4c

+ 6 - 0
src/main/java/password/pwm/config/Configuration.java

@@ -44,6 +44,7 @@ import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.config.profile.UpdateAttributesProfile;
 import password.pwm.config.stored.ConfigurationProperty;
+import password.pwm.config.value.CustomLinkValue;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.BooleanValue;
@@ -263,6 +264,11 @@ public class Configuration implements Serializable, SettingReader {
             if (value == null) {
                 return null;
             }
+
+            if (value instanceof CustomLinkValue) {
+                return (List<FormConfiguration>)value.toNativeObject();
+            }
+
             if ((!(value instanceof FormValue))) {
                 throw new IllegalArgumentException("setting value is not readable as form");
             }

+ 119 - 0
src/main/java/password/pwm/config/CustomLinkConfiguration.java

@@ -0,0 +1,119 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.config;
+
+import password.pwm.i18n.Display;
+import password.pwm.util.LocaleHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * @author Richard A. Keil
+ */
+public class CustomLinkConfiguration implements Serializable {
+// ------------------------------ FIELDS ------------------------------
+
+    public enum Type {text, url, select, checkbox, customLink}
+
+    private String name;
+    private Type type = Type.customLink;
+    private Map<String,String> labels = Collections.singletonMap("", "");
+    private Map<String,String> description = Collections.singletonMap("","");
+    private String customLinkUrl = "";
+    private boolean customLinkNewWindow;
+    private Map<String,String> selectOptions = Collections.emptyMap();
+
+// -------------------------- STATIC METHODS --------------------------
+
+
+// --------------------- GETTER / SETTER METHODS ---------------------
+
+    public String getName() {
+        return name;
+    }
+
+    public String getLabel(final Locale locale) {
+        return LocaleHelper.resolveStringKeyLocaleMap(locale, labels);
+    }
+
+    public String getDescription(final Locale locale) {
+        return LocaleHelper.resolveStringKeyLocaleMap(locale, description);
+    }
+
+    public Map<String,String> getLabelDescriptionLocaleMap() {
+        return Collections.unmodifiableMap(this.description);
+    }
+
+    public Type getType() {
+        return type;
+    }
+
+    public boolean isCustomLinkNewWindow() {
+        return customLinkNewWindow;
+    }
+
+    public String getcustomLinkUrl() {
+        return customLinkUrl;
+    }
+
+    public Map<String,String> getSelectOptions() {
+        return Collections.unmodifiableMap(selectOptions);
+    }
+
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+
+        sb.append("FormItem: ");
+        sb.append(JsonUtil.serialize(this));
+
+        return sb.toString();
+    }
+
+// -------------------------- OTHER METHODS --------------------------
+
+    public String displayValue(final String value, final Locale locale, final Configuration config) {
+        if (value == null) {
+            return LocaleHelper.getLocalizedMessage(locale, Display.Value_NotApplicable, config);
+        }
+
+        if (this.getType() == Type.select) {
+            if (this.getSelectOptions() != null) {
+                for (final String key : selectOptions.keySet()) {
+                    if (value.equals(key)) {
+                        final String displayValue = selectOptions.get(key);
+                        if (!StringUtil.isEmpty(displayValue)) {
+                            return displayValue;
+                        }
+                    }
+                }
+            }
+        }
+
+        return value;
+    }
+}

+ 2 - 0
src/main/java/password/pwm/config/PwmSetting.java

@@ -867,6 +867,8 @@ public enum PwmSetting {
     UPDATE_PROFILE_SMS_VERIFICATION(
             "updateAttributes.sms.verification", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.UPDATE_PROFILE),
 
+    UPDATE_PROFILE_CUSTOMLINKS(
+            "updateAttributes.customLinks", PwmSettingSyntax.CUSTOMLINKS, PwmSettingCategory.UPDATE_PROFILE),
 
     // shortcut settings
     SHORTCUT_ENABLE(

+ 1 - 0
src/main/java/password/pwm/config/PwmSettingSyntax.java

@@ -71,6 +71,7 @@ public enum PwmSettingSyntax {
     VERIFICATION_METHOD(VerificationMethodValue.factory()),
     PRIVATE_KEY(PrivateKeyValue.factory()),
     NAMED_SECRET(NamedSecretValue.factory()),
+    CUSTOMLINKS(CustomLinkValue.factory()),
     REMOTE_WEB_SERVICE(RemoteWebServiceValue.factory()),
 
     ;

+ 137 - 0
src/main/java/password/pwm/config/value/CustomLinkValue.java

@@ -0,0 +1,137 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.config.value;
+
+import com.google.gson.reflect.TypeToken;
+import org.jdom2.Element;
+import password.pwm.config.CustomLinkConfiguration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.StoredValue;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.secure.PwmSecurityKey;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+public class CustomLinkValue extends AbstractValue implements StoredValue {
+    final List<CustomLinkConfiguration> values;
+
+    public CustomLinkValue(final List<CustomLinkConfiguration> values) {
+        this.values = values;
+    }
+
+    public static StoredValueFactory factory()
+    {
+        return new StoredValueFactory() {
+            public CustomLinkValue fromJson(final String input)
+            {
+                if (input == null) {
+                    return new CustomLinkValue(Collections.<CustomLinkConfiguration>emptyList());
+                } else {
+                    List<CustomLinkConfiguration> srcList = JsonUtil.deserialize(input, new TypeToken<List<CustomLinkConfiguration>>() {
+                    });
+                    srcList = srcList == null ? Collections.<CustomLinkConfiguration>emptyList() : srcList;
+                    while (srcList.contains(null)) {
+                        srcList.remove(null);
+                    }
+                    return new CustomLinkValue(Collections.unmodifiableList(srcList));
+                }
+            }
+
+            public CustomLinkValue fromXmlElement(final Element settingElement, final PwmSecurityKey key)
+                    throws PwmOperationalException
+            {
+                final List valueElements = settingElement.getChildren("value");
+                final List<CustomLinkConfiguration> values = new ArrayList<>();
+                for (final Object loopValue : valueElements) {
+                    final Element loopValueElement = (Element) loopValue;
+                    final String value = loopValueElement.getText();
+                    if (value != null && value.length() > 0 && loopValueElement.getAttribute("locale") == null) {
+                        values.add(JsonUtil.deserialize(value, CustomLinkConfiguration.class));
+                    }
+                }
+                final CustomLinkValue CustomLinkValue = new CustomLinkValue(values);
+                return CustomLinkValue;
+            }
+        };
+    }
+
+    public List<Element> toXmlValues(final String valueElementName) {
+        final List<Element> returnList = new ArrayList<>();
+        for (final CustomLinkConfiguration value : values) {
+            final Element valueElement = new Element(valueElementName);
+            valueElement.addContent(JsonUtil.serialize(value));
+            returnList.add(valueElement);
+        }
+        return returnList;
+    }
+
+    public List<CustomLinkConfiguration> toNativeObject() {
+        return Collections.unmodifiableList(values);
+    }
+
+    public List<String> validateValue(final PwmSetting pwmSetting) {
+        if (pwmSetting.isRequired()) {
+            if (values == null || values.size() < 1 || values.get(0) == null) {
+                return Collections.singletonList("required value missing");
+            }
+        }
+
+        final Set<String> seenNames = new HashSet<>();
+        for (final CustomLinkConfiguration loopConfig : values) {
+            if (seenNames.contains(loopConfig.getName().toLowerCase())) {
+                return Collections.singletonList("each form name must be unique: " + loopConfig.getName());
+            }
+            seenNames.add(loopConfig.getName().toLowerCase());
+        }
+
+        return Collections.emptyList();
+    }
+
+    public String toDebugString(final Locale locale) {
+        if (values != null && !values.isEmpty()) {
+            final StringBuilder sb = new StringBuilder();
+            for (final CustomLinkConfiguration formRow : values) {
+                sb.append("FormItem Name:").append(formRow.getName()).append("\n");
+                sb.append(" Type:").append(formRow.getType());
+                sb.append("\n");
+                sb.append(" Description:").append(JsonUtil.serializeMap(formRow.getLabelDescriptionLocaleMap())).append("\n");
+                sb.append(" Name:").append(formRow.isCustomLinkNewWindow()).append("\n");
+                sb.append(" Name:").append(formRow.getcustomLinkUrl()).append("\n");
+                if (formRow.getSelectOptions() != null && !formRow.getSelectOptions().isEmpty()) {
+                    sb.append(" Select Options: ").append(JsonUtil.serializeMap(formRow.getSelectOptions())).append("\n");
+                }
+
+            }
+            return sb.toString();
+        } else {
+            return "";
+        }
+    }
+
+}

+ 7 - 0
src/main/java/password/pwm/http/PwmRequest.java

@@ -530,6 +530,13 @@ public class PwmRequest extends PwmHttpRequestWrapper implements Serializable {
         this.setAttribute(PwmRequestAttribute.FormData, formDataMapValue);
         this.setAttribute(PwmRequestAttribute.FormReadOnly, readOnly);
         this.setAttribute(PwmRequestAttribute.FormShowPasswordFields, showPasswordFields);
+        this.setAttribute(PwmRequestAttribute.FormMobileDevices, formDataMapValue);
+        this.setAttribute(PwmRequestAttribute.FormCustomLinks, new ArrayList<>(formConfiguration));
+    }
+
+    public void addFormInfoToRequestAttr(
+            final List<FormConfiguration> FormCustomLinks) {
+        this.setAttribute(PwmRequestAttribute.FormCustomLinks, new ArrayList<>(FormCustomLinks));
     }
 
     public void invalidateSession() {

+ 2 - 0
src/main/java/password/pwm/http/PwmRequestAttribute.java

@@ -42,6 +42,8 @@ public enum PwmRequestAttribute {
     FormReadOnly,
     FormShowPasswordFields,
     FormData,
+    FormMobileDevices,
+    FormCustomLinks,
 
     SetupResponses_ResponseInfo,
 

+ 4 - 0
src/main/java/password/pwm/http/servlet/UpdateProfileServlet.java

@@ -571,6 +571,8 @@ public class UpdateProfileServlet extends ControlledPwmServlet {
         final List<FormConfiguration> form = updateAttributesProfile.readSettingAsForm(PwmSetting.UPDATE_PROFILE_FORM);
         final Map<FormConfiguration,String> formValueMap = formMapFromBean(updateAttributesProfile, updateProfileBean);
         pwmRequest.addFormInfoToRequestAttr(form, formValueMap, false, false);
+        final List<FormConfiguration> links = updateAttributesProfile.readSettingAsForm(PwmSetting.UPDATE_PROFILE_CUSTOMLINKS);
+        pwmRequest.addFormInfoToRequestAttr(links);
         pwmRequest.forwardToJsp(JspUrl.UPDATE_ATTRIBUTES);
     }
 
@@ -580,6 +582,8 @@ public class UpdateProfileServlet extends ControlledPwmServlet {
         final List<FormConfiguration> form = updateAttributesProfile.readSettingAsForm(PwmSetting.UPDATE_PROFILE_FORM);
         final Map<FormConfiguration,String> formValueMap = formMapFromBean(updateAttributesProfile, updateProfileBean);
         pwmRequest.addFormInfoToRequestAttr(form, formValueMap, true, false);
+        final List<FormConfiguration> links = updateAttributesProfile.readSettingAsForm(PwmSetting.UPDATE_PROFILE_CUSTOMLINKS);
+        pwmRequest.addFormInfoToRequestAttr(links);
         pwmRequest.forwardToJsp(JspUrl.UPDATE_ATTRIBUTES_CONFIRM);
     }
 

+ 5 - 1
src/main/resources/password/pwm/config/PwmSetting.xml

@@ -2933,7 +2933,11 @@
             <value>false</value>
         </default>
     </setting>
-
+    <setting hidden="false" key="updateAttributes.customLinks" level="1">
+        <default>
+            <value></value>
+        </default>
+    </setting>
     <setting hidden="false" key="shortcut.enable" level="1">
         <default>
             <value>false</value>

+ 3 - 0
src/main/resources/password/pwm/i18n/Config.properties

@@ -156,6 +156,9 @@ Tooltip_FormOptions_ReadOnly=Make the field unmodifiable.
 Tooltip_FormOptions_Unique=Indicate that the field value must be unique in the directory before proceeding.
 Tooltip_FormOptions_Regex=Apply a regular expression pattern to the value.  The value must match the pattern before the form is completed.  This pattern can be used to constrain the permitted syntax of the value.
 Tooltip_FormOptions_RegexError=Error message to show when the regular expression pattern is not matched.
+Tooltip_FormOptions_LinkLabel=Label to be displayed that tells where the link will go.
+Tooltip_FormOptions_LinkURL=Full url that you want to go to when the link is selected.
+Tooltip_Form_ShowInNewWindow=Choose if the link will e opened in a new browser window
 Tooltip_FormOptions_Placeholder=Placeholder text to display in the form field with the field is not populated with a value.
 Tooltip_FormOptions_Javascript=Javascript to be added to the browser.
 Tooltip_FormOptions_MultiValue=Display multiple values of the attribute.

+ 9 - 3
src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -191,6 +191,7 @@ Category_Label_REST_SERVER=REST Services
 Category_Label_SECURITY=Security
 Category_Label_SETTINGS=Settings
 Category_Label_SHORTCUT=Shortcut Menu
+Category_Label_PROFILE=Link Menu
 Category_Label_SMS_GATEWAY=SMS Gateway
 Category_Label_SMS_MESSAGES=SMS Messages
 Category_Label_SMS=SMS
@@ -506,7 +507,7 @@ Setting_Description_password.policy.ADComplexityMaxViolations=Specify the maximu
 Setting_Description_password.policy.allowFirstCharNumeric=Enable this option to allow the first character of the password to be numeric.  Applies only if the password policy allows numeric characters.
 Setting_Description_password.policy.allowFirstCharSpecial=Enable this option to allow the first character of the password to be a special character.  Applies only if the password policy allows special characters.
 Setting_Description_password.policy.allowLastCharNumeric=Enable this option to allow the last character of the password to be numeric.  Applies only if the password policy allows numeric characters.
-Setting_Description_password.policy.allowLastCharSpecial=Enalbe this option to allow the last character of the password to be a special character.  Applies only if the password policy allows special characters.
+Setting_Description_password.policy.allowLastCharSpecial=Enable this option to allow the last character of the password to be a special character.  Applies only if the password policy allows special characters.
 Setting_Description_password.policy.allowNumeric=Enable this option to allow numeric characters in the password.
 Setting_Description_password.policy.allowSpecial=Enable this option to allow special (non alpha-numeric) characters in the password.
 Setting_Description_password.policy.caseSensitivity=Enable this option to control if the password is case sensitive.  In most cases, @PwmAppName@ can read this from the directory, but in some cases, the system cannot correctly read this value, so you can override it here.
@@ -587,7 +588,7 @@ Setting_Description_pwm.versionCheck.enable=Enable this option to allow for peri
 Setting_Description_pwm.wordlist.location=Specify a word list file URL for dictionary checking to prevent users from using commonly used words as passwords.   Using word lists is an important part of password security.  Word lists are used by intruders to guess common passwords.   The default word list included contains commonly used English passwords.  <br/><br/>The first time a startup occurs with a new word list setting, it takes some time to compile the word list into a database.  See the status screen and logs for progress information.  The word list file format is one or more text files containing a single word per line, enclosed in a ZIP file.  The String <i>\!\#comment\:</i> at the beginning of a line indicates a comment. <br/><br/>The value must be a valid URL, using the protocol "file" (local file system), "http", or "https".
 Setting_Description_recovery.action=Add actions to take when the user completes the forgotten password process.
 Setting_Description_recovery.allowWhenLocked=Enable this option to allow users to use the forgotten password feature when the account is intruder locked in LDAP.  This feature is not available when a user is using NMAS stored responses.
-Setting_Description_recovery.enable=Enalbe this option to have the forgotten password recovery available to users.
+Setting_Description_recovery.enable=Enable this option to have the forgotten password recovery available to users.
 Setting_Description_recovery.form=Specify the form fields for the activate user module. @PwmAppName@ requires the users to enter each attribute. Ideally, @PwmAppName@ requires the users to enter some personal data that is not publicly known.
 Setting_Description_recovery.oauth.idserver.attributesUrl=Specify the web service URL provided by the identity server to return attribute data about the user.
 Setting_Description_recovery.oauth.idserver.clientName=Specify the OAuth client ID. The OAuth identity service provider gives you this value.
@@ -664,7 +665,7 @@ Setting_Description_token.length=Specify the length of the email token
 Setting_Description_token.lifetime=Specify how long a lifetime token is valid (in seconds). The default is one hour.
 Setting_Description_token.storageMethod=Select the storage method @PwmAppName@ uses to save issued tokens.<table style\="width\: 400px"><tr><td>Method</td><td>Description</td></tr><tr><td>LocalDB</td><td>Stores the tokens in the local embedded LocalDB database.  Tokens are not common across multiple application instances.</td></tr><tr><td>DB</td><td>Store the tokens in a configured, remote database.  Tokens work across multiple application instances.</td></tr><tr><td>Crypto</td><td>Use crypto to create and read tokens, they are not stored locally.  Tokens work across multiple application instances if they have the same Security Key.  Crypto tokens ignore the length rules and might be too long to use for SMS purposes.</td></tr><tr><td>LDAP</td><td>Use the LDAP directory to store tokens.  Tokens work across multiple application instances.  You cannot use LDAP tokens as New User Registration tokens.</td></tr></table>
 Setting_Description_updateAttributes.check.queryMatch=When you use the "checkProfile" or "checkAll" parameter with the command servlet, @PwmAppName@ uses this query match to determine if the user is required to populate the parameter values. <br/><br/>If this value is blank, then @PwmAppName@ checks the user's current values against the form requirements.
-Setting_Description_updateAttributes.email.verification=Enalbe this option to send an email to the user's email address before @PwmAppName@ updates the account.  The user must verify receipt of the email before @PwmAppName@ updates the account.
+Setting_Description_updateAttributes.email.verification=Enable this option to send an email to the user's email address before @PwmAppName@ updates the account.  The user must verify receipt of the email before @PwmAppName@ updates the account.
 Setting_Description_updateAttributes.enable=Enable the option to Update Profile Attributes.  If true, this setting enables the Update Profile module.
 Setting_Description_updateAttributes.forceSetup=Enable this option to present the Update Profile module to the users upon login if the users do not satisfy the form configuration conditions. Specifically, @PwmAppName@ checks the <b>Required</b> and <b>Regular Expression</b> conditions against the current LDAP form values.  The users cannot perform other functions until they update the form values to values that match the form configuration.
 Setting_Description_updateAttributes.form=Update Profile Form values.
@@ -672,6 +673,7 @@ Setting_Description_updateAttributes.profile.list=Update Attributes Profiles
 Setting_Description_updateAttributes.queryMatch=Add an LDAP query that only allows users who match this query to update their profiles.
 Setting_Description_updateAttributes.showConfirmation=Enable this option to show the update attributes to the users after they configure them.  This gives your users an opportunity to read and review their attributes before submitting, however, it shows the responses on the screen and makes them visible to anyone else watching the users' screens.
 Setting_Description_updateAttributes.sms.verification=Enable this option to send an SMS to the users' mobile phone numbers before updating the account.  The user must verify receipt of the SMS before @PwmAppName@ updates the account.
+Setting_Description_updateAttributes.customLinks=Create custom links for users to update their profile data.
 Setting_Description_updateAttributes.writeAttributes=Add actions to execute after @PwmAppName@ populates a user's attributes.
 Setting_Description_urlshortener.classname=Specify the URL Shortening Service class name. The Java full class name that implements a short URL service. You must include the corresponding JAR or ZIP file in the classpath, typically in the <i>WEB-INF/lib</i> directory or the application server's lib directory.
 Setting_Description_urlshortener.parameters=Specify the Name/Value settings used to configure the selected URL shortening service. For example, an API key, user name, password or domain name. The settings must be in "name\=value" format, where name is the key value of a valid service setting.
@@ -774,6 +776,7 @@ Setting_Label_display.showHidePasswordFields=Enable Showing Masked Fields
 Setting_Label_display.showLoginPageOptions=Show Login Page Options
 Setting_Label_display.showSuccessPage=Show Success Pages
 Setting_Label_display.updateAttributes.agreement=Update Profile Agreement Message
+Setting_Label_display.updateAttributes.preferredlanguage=Select your preferred language
 Setting_Label_email.activation=Activation Email
 Setting_Label_email.activation.token=Activation Verification Email
 Setting_Label_email.adminAlert.toAddress=System Audit Event Email Alerts
@@ -953,6 +956,7 @@ Setting_Label_newUser.profile.visible=Profile Visible on Menu
 Setting_Label_newUser.promptForPassword=Prompt User for Password
 Setting_Label_newUser.redirectUrl=After Registration Redirect URL
 Setting_Label_newUser.sms.verification=Enable New User SMS Verification
+Setting_Label_newUser.customLinks=Enable New User Custom links
 Setting_Label_newUser.username.definition=LDAP Entry ID Definition
 Setting_Label_newUser.writeAttributes=New User Actions
 Setting_Label_notes.noteText=Configuration Notes
@@ -1144,10 +1148,12 @@ Setting_Label_updateAttributes.email.verification=Enable Email Verification
 Setting_Label_updateAttributes.enable=Enable Update Profile
 Setting_Label_updateAttributes.forceSetup=Force Update Profile
 Setting_Label_updateAttributes.form=Update Profile Form
+Setting_Label_updateAttributes.preferredlanguage=Update Profile language
 Setting_Label_updateAttributes.profile.list=List of Update Attribute profiles.  In most cases, only a single profile is needed.  Only define multiple profiles if different user populations users will need different features/permissions.  Each profile has a <i>Update Attributes Profile Match</i> setting used to define to whom the profile applies.  If multiple profiles could apply for a user, the first profile in the list defined here will be assigned.
 Setting_Label_updateAttributes.queryMatch=Update Profile Match
 Setting_Label_updateAttributes.showConfirmation=Show Update Profile Confirmation
 Setting_Label_updateAttributes.sms.verification=Enable SMS Verification
+Setting_Label_updateAttributes.customLinks=Update profile Custom Links 
 Setting_Label_updateAttributes.writeAttributes=Update Profile Actions
 Setting_Label_urlshortener.classname=Enable URL Shortening Service Class
 Setting_Label_urlshortener.parameters=Configuration Parameters for URL Shortening Service

+ 17 - 5
src/main/webapp/WEB-INF/jsp/fragment/form.jsp

@@ -14,6 +14,7 @@
 <%@ page import="password.pwm.http.tag.value.PwmValue" %>
 <%@ page import="password.pwm.http.PwmRequestAttribute" %>
 <%@ page import="java.util.Collections" %>
+<%@ page import="password.pwm.config.CustomLinkConfiguration" %>
 
 <%--
   ~ Password Management Servlets (PWM)
@@ -38,10 +39,22 @@
   --%>
 
 <%@ taglib uri="pwm" prefix="pwm" %>
-<%
-    final PwmRequest formPwmRequest = PwmRequest.forRequest(request,response);
-    final List<FormConfiguration> formConfigurationList = (List<FormConfiguration>)JspUtility.getAttribute(pageContext, PwmRequestAttribute.FormConfiguration);
-%>
+
+    <% final PwmRequest formPwmRequest = PwmRequest.forRequest(request,response); %>
+    <% final List<FormConfiguration> formConfigurationList = (List<FormConfiguration>)JspUtility.getAttribute(pageContext, PwmRequestAttribute.FormConfiguration); %>
+    <% final List<CustomLinkConfiguration> linkConfigurationList = (List<CustomLinkConfiguration>)JspUtility.getAttribute(pageContext, PwmRequestAttribute.FormCustomLinks); %>
+    <% final Locale formLocale = formPwmRequest.getLocale(); %>
+    <% if (linkConfigurationList != null) { %>
+        <% for (final CustomLinkConfiguration loopList : linkConfigurationList) { %>
+           <% if (loopList.isCustomLinkNewWindow()) { %>
+                <a href=<%=loopList.getcustomLinkUrl()%> title=<%=loopList.getDescription(formLocale)%> target="_blank"><%=loopList.getLabel(formLocale)%></a><br>
+            <% } else { %>
+                <a href=<%=loopList.getcustomLinkUrl()%> title=<%=loopList.getDescription(formLocale)%>><%=loopList.getLabel(formLocale)%></a><br>
+            <% } %>
+        <% } %>
+    <% } %>
+
+    <hr>
 <% if (formConfigurationList == null) { %>
 [ form definition is not available ]
 <% } else if (formConfigurationList.isEmpty()) { %>
@@ -56,7 +69,6 @@
             : Collections.<String,String>emptyMap();
 
     final PwmApplication pwmApplication = formPwmRequest.getPwmApplication();
-    final Locale formLocale = formPwmRequest.getLocale();
     for (final FormConfiguration loopConfiguration : formConfigurationList) {
         String currentValue = formDataMap != null ? formDataMap.get(loopConfiguration) : "";
         currentValue = currentValue == null ? "" : currentValue;

+ 4 - 1
src/main/webapp/public/resources/js/changepassword.js

@@ -248,7 +248,10 @@ PWM_CHANGEPW.doRandomGeneration=function(randomConfig) {
                 eventHandlers.push(function(){
                     PWM_MAIN.addEventHandler(elementID,'click',function(){
                         var value = PWM_MAIN.getObject(elementID).innerHTML;
-                        finishAction(value);
+                        var parser = new DOMParser();
+                        var dom = parser.parseFromString(value, 'text/html');
+                        var domString = dom.body.textContent;
+                        finishAction(domString);
                     });
                 });
             })(i);

+ 400 - 0
src/main/webapp/public/resources/js/configeditor-settings.js

@@ -3321,3 +3321,403 @@ NamedSecretHandler.deletePassword = function(settingKey, key) {
     });
 
 };
+
+
+// -------------------------- Custom link handler ------------------------------------
+
+var CustomLinkHandler = {};
+CustomLinkHandler.newRowValue = {
+    name:'',
+    labels:{'':''},
+    description:{'':''}
+};
+
+CustomLinkHandler.init = function(keyName) {
+    console.log('CustomLinkHandler init for ' + keyName);
+    var parentDiv = 'table_setting_' + keyName;
+    PWM_CFGEDIT.clearDivElements(parentDiv, true);
+    PWM_CFGEDIT.readSetting(keyName, function(resultValue) {
+        PWM_VAR['clientSettingCache'][keyName] = resultValue;
+        CustomLinkHandler.redraw(keyName);
+    });
+};
+
+CustomLinkHandler.redraw = function(keyName) {
+    var resultValue = PWM_VAR['clientSettingCache'][keyName];
+    var parentDiv = 'table_setting_' + keyName;
+    var parentDivElement = PWM_MAIN.getObject(parentDiv);
+
+    parentDivElement.innerHTML = '<table class="noborder" style="margin-left: 0; width:auto" id="table-top-' + keyName + '"></table>';
+    parentDiv = 'table-top-' + keyName;
+    parentDivElement = PWM_MAIN.getObject(parentDiv);
+
+    if (!PWM_MAIN.JSLibrary.isEmpty(resultValue)) {
+        var headerRow = document.createElement("tr");
+        var rowHtml = '<td>Name</td><td></td><td>Label</td>';
+        headerRow.innerHTML = rowHtml;
+        parentDivElement.appendChild(headerRow);
+    }
+
+    for (var i in resultValue) {
+        CustomLinkHandler.drawRow(parentDiv, keyName, i, resultValue[i]);
+    }
+
+    var buttonRow = document.createElement("tr");
+    buttonRow.setAttribute("colspan","5");
+    buttonRow.innerHTML = '<td><button class="btn" id="button-' + keyName + '-addRow"><span class="btn-icon pwm-icon pwm-icon-plus-square"></span>Add Item</button></td>';
+
+    parentDivElement.appendChild(buttonRow);
+
+    PWM_MAIN.addEventHandler('button-' + keyName + '-addRow','click',function(){
+        CustomLinkHandler.addRow(keyName);
+    });
+
+};
+
+CustomLinkHandler.drawRow = function(parentDiv, settingKey, iteration, value) {
+    require(["dojo/json"], function(JSON){
+        var itemCount = PWM_MAIN.JSLibrary.itemCount(PWM_VAR['clientSettingCache'][settingKey]);
+        var inputID = 'value_' + settingKey + '_' + iteration + "_";
+        var options = PWM_SETTINGS['settings'][settingKey]['options'];
+        var properties = PWM_SETTINGS['settings'][settingKey]['properties'];
+
+        var newTableRow = document.createElement("tr");
+        newTableRow.setAttribute("style", "border-width: 0");
+
+        var htmlRow = '';
+        htmlRow += '<td style="background: #f6f9f8; border:1px solid #dae1e1; width:180px"><div class="noWrapTextBox" id="panel-name-' + inputID + '" ></div></td>';
+        htmlRow += '<td style="width:1px" id="icon-editLabel-' + inputID + '"><span class="btn-icon pwm-icon pwm-icon-edit"></span></td>';
+        htmlRow += '<td style="background: #f6f9f8; border:1px solid #dae1e1; width:170px"><div style="" class="noWrapTextBox " id="' + inputID + 'label"><span>' + value['labels'][''] + '</span></div></td>';
+
+        var userDNtypeAllowed = options['type-userDN'] == 'show';
+        if (!PWM_MAIN.JSLibrary.isEmpty(options)) {
+            htmlRow += '<td style="width:15px;">';
+            htmlRow += '<select id="' + inputID + 'type">';
+            for (var optionItem in options) {
+                //if (optionList[optionItem] != 'userDN' || userDNtypeAllowed) {
+                var optionName = options[optionItem];
+                var selected = (optionName == PWM_VAR['clientSettingCache'][settingKey][iteration]['type']);
+                htmlRow += '<option value="' + optionName + '"' + (selected ? " selected" : "") + '>' + optionName + '</option>';
+                //}
+            }
+            htmlRow += '</select>';
+            htmlRow += '</td>';
+        }
+
+        var hideOptions = PWM_MAIN.JSLibrary.arrayContains(PWM_SETTINGS['settings'][settingKey]['flags'], 'Form_HideOptions');
+        if (!hideOptions) {
+            htmlRow += '<td class="noborder" style="min-width:90px;"><button id="' + inputID + 'optionsButton"><span class="btn-icon pwm-icon pwm-icon-sliders"/> Options</button></td>';
+        }
+
+        htmlRow += '<td style="width:10px">';
+        if (itemCount > 1 && iteration != (itemCount -1)) {
+            htmlRow += '<span id="' + inputID + '-moveDown" class="action-icon pwm-icon pwm-icon-chevron-down"></span>';
+        }
+        htmlRow += '</td>';
+
+        htmlRow += '<td style="width:10px">';
+        if (itemCount > 1 && iteration != 0) {
+            htmlRow += '<span id="' + inputID + '-moveUp" class="action-icon pwm-icon pwm-icon-chevron-up"></span>';
+        }
+        htmlRow += '</td>';
+        htmlRow += '<td style="width:10px"><span class="delete-row-icon action-icon pwm-icon pwm-icon-times" id="' + inputID + '-deleteRowButton"></span></td>';
+
+        newTableRow.innerHTML = htmlRow;
+        var parentDivElement = PWM_MAIN.getObject(parentDiv);
+        parentDivElement.appendChild(newTableRow);
+
+        UILibrary.addTextValueToElement("panel-name-" + inputID,value['name']);
+
+        PWM_MAIN.addEventHandler(inputID + "-moveUp", 'click', function () {
+            CustomLinkHandler.move(settingKey, true, iteration);
+        });
+        PWM_MAIN.addEventHandler(inputID + "-moveDown", 'click', function () {
+            CustomLinkHandler.move(settingKey, false, iteration);
+        });
+        PWM_MAIN.addEventHandler(inputID + "-deleteRowButton", 'click', function () {
+            CustomLinkHandler.removeRow(settingKey, iteration);
+        });
+        PWM_MAIN.addEventHandler(inputID + "label", 'click, keypress', function () {
+            CustomLinkHandler.showLabelDialog(settingKey, iteration);
+        });
+       PWM_MAIN.addEventHandler("icon-editLabel-" + inputID, 'click, keypress', function () {
+            CustomLinkHandler.showLabelDialog(settingKey, iteration);
+        });
+        PWM_MAIN.addEventHandler(inputID + "optionsButton", 'click', function () {
+            CustomLinkHandler.showOptionsDialog(settingKey, iteration);
+        });
+        PWM_MAIN.addEventHandler(inputID + "name", 'input', function () {
+            PWM_VAR['clientSettingCache'][settingKey][iteration]['name'] = PWM_MAIN.getObject(inputID + "name").value;
+            CustomLinkHandler.write(settingKey);
+        });
+        PWM_MAIN.addEventHandler(inputID + "type", 'click', function () {
+            PWM_VAR['clientSettingCache'][settingKey][iteration]['type'] = PWM_MAIN.getObject(inputID + "type").value;
+            CustomLinkHandler.write(settingKey);
+        });
+    });
+};
+
+CustomLinkHandler.write = function(settingKey, finishFunction) {
+    var cachedSetting = PWM_VAR['clientSettingCache'][settingKey];
+    PWM_CFGEDIT.writeSetting(settingKey, cachedSetting, finishFunction);
+};
+
+CustomLinkHandler.removeRow = function(keyName, iteration) {
+    PWM_MAIN.showConfirmDialog({
+        text:'Are you sure you wish to delete this item?',
+        okAction:function(){
+            var currentValues = PWM_VAR['clientSettingCache'][keyName];
+            currentValues.splice(iteration,1);
+            CustomLinkHandler.write(keyName,function(){
+                CustomLinkHandler.init(keyName);
+            });
+        }
+    });
+};
+
+CustomLinkHandler.move = function(settingKey, moveUp, iteration) {
+    var currentValues = PWM_VAR['clientSettingCache'][settingKey];
+    if (moveUp) {
+        CustomLinkHandler.arrayMoveUtil(currentValues, iteration, iteration - 1);
+    } else {
+        CustomLinkHandler.arrayMoveUtil(currentValues, iteration, iteration + 1);
+    }
+    CustomLinkHandler.write(settingKey);
+    CustomLinkHandler.redraw(settingKey);
+};
+
+CustomLinkHandler.arrayMoveUtil = function(arr, fromIndex, toIndex) {
+    var element = arr[fromIndex];
+    arr.splice(fromIndex, 1);
+    arr.splice(toIndex, 0, element);
+};
+
+
+CustomLinkHandler.addRow = function(keyName) {
+    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;
+                }
+            }
+            var currentSize = PWM_MAIN.JSLibrary.itemCount(PWM_VAR['clientSettingCache'][keyName]);
+            PWM_VAR['clientSettingCache'][keyName][currentSize + 1] = CustomLinkHandler.newRowValue;
+            PWM_VAR['clientSettingCache'][keyName][currentSize + 1].name = value;
+            PWM_VAR['clientSettingCache'][keyName][currentSize + 1].labels = {'':value};
+            CustomLinkHandler.write(keyName,function(){
+                CustomLinkHandler.init(keyName);
+            });
+        }
+    });
+};
+
+CustomLinkHandler.showOptionsDialog = function(keyName, iteration) {
+    var type = PWM_VAR['clientSettingCache'][keyName][iteration]['type'];
+    var settings = PWM_SETTINGS['settings'][keyName];
+    var options = 'options' in PWM_SETTINGS['settings'][keyName] ? PWM_SETTINGS['settings'][keyName]['options'] : {};
+
+    require(["dijit/Dialog", "dijit/form/Textarea", "dijit/form/CheckBox", "dijit/form/NumberSpinner"], function () {
+        var inputID = 'value_' + keyName + '_' + iteration + '_';
+        var bodyText = '<div style="max-height: 500px; overflow-x: auto"><table class="noborder">';
+
+        bodyText += '<tr>';
+        var descriptionValue = PWM_VAR['clientSettingCache'][keyName][iteration]['description'][''];
+        bodyText += '<td id="' + inputID + '-label-description" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_Description') + '">Description</td><td>';
+        bodyText += '<div class="noWrapTextBox" id="' + inputID + 'DescriptionValue"><span class="btn-icon pwm-icon pwm-icon-edit"></span><span>' + descriptionValue + '...</span></div>';
+        bodyText += '</td>';
+
+        bodyText += '</tr><tr>';
+
+        var customUrl = PWM_VAR['clientSettingCache'][keyName][iteration]['customLinkUrl'];
+        bodyText += '<td id="' + inputID + '-Site-url" class="key" title="' + PWM_CONFIG.showString('Tooltip_FormOptions_LinkURL') + '">Link URL</td><td>' +
+            '<input placeholder="https://example.com" style="width: 350px;" type="url" class="key" id="' + inputID + 'SiteURL' + '" value="'+ customUrl +'"/></td>';
+        bodyText += '</tr><tr>';
+
+        var checkedValue = PWM_VAR['clientSettingCache'][keyName][iteration]['customLinkNewWindow'];
+        bodyText += '<td id="' + inputID + '-NewWindow-Option" class="key" title="' + PWM_CONFIG.showString('Tooltip_Form_ShowInNewWindow') + '">Open link in new window</td><td><input type="checkbox" id="' + inputID + 'customLinkNewWindow' + '" ';
+        if(checkedValue) {
+            bodyText += 'checked'
+        }
+        bodyText += '/></td>';
+
+        bodyText += '</tr><tr>';
+
+        bodyText += '</table></div>';
+
+        var initDialogWidgets = function () {
+
+            PWM_MAIN.clearDijitWidget(inputID + "DescriptionValue");
+            PWM_MAIN.addEventHandler(inputID + 'DescriptionValue', 'change', function () {
+                CustomLinkHandler.showDescriptionDialog(keyName, iteration);
+            });
+
+            PWM_MAIN.addEventHandler(inputID + 'DescriptionValue', 'click', function () {
+                CustomLinkHandler.showDescriptionDialog(keyName, iteration);
+            });
+
+            PWM_MAIN.clearDijitWidget(inputID + "SiteURL");
+            PWM_MAIN.addEventHandler(inputID + 'SiteURL', 'change', function () {
+                PWM_VAR['clientSettingCache'][keyName][iteration]['customLinkUrl'] = this.value;
+                CustomLinkHandler.write(keyName)
+            });
+
+            PWM_MAIN.clearDijitWidget(inputID + "customLinkNewWindow");
+            PWM_MAIN.addEventHandler(inputID + 'customLinkNewWindow', 'click', function () {
+                if(this.value === "on")
+                    this.value = "true";
+                PWM_VAR['clientSettingCache'][keyName][iteration]['customLinkNewWindow'] = this.value;
+                CustomLinkHandler.write(keyName)
+            });
+        };
+
+        PWM_MAIN.showDialog({
+            title: PWM_SETTINGS['settings'][keyName]['label'] + ' - ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'],
+            text: bodyText,
+            allowMove: true,
+            loadFunction: initDialogWidgets,
+            okAction: function () {
+                CustomLinkHandler.redraw(keyName);
+            }
+        });
+    });
+};
+
+CustomLinkHandler.showLabelDialog = function(keyName, iteration) {
+    var finishAction = function(){ CustomLinkHandler.redraw(keyName); };
+    var title = 'Label for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'];
+    CustomLinkHandler.multiLocaleStringDialog(keyName, iteration, 'labels', finishAction, title);
+};
+
+CustomLinkHandler.multiLocaleStringDialog = function(keyName, iteration, settingType, finishAction, titleText) {
+    require(["dijit/Dialog","dijit/form/Textarea","dijit/form/CheckBox"],function(){
+        var inputID = 'value_' + keyName + '_' + iteration + "_" + "label_";
+        var bodyText = '<table class="noborder" id="' + inputID + 'table">';
+        bodyText += '<tr>';
+        for (var localeName in PWM_VAR['clientSettingCache'][keyName][iteration][settingType]) {
+            var value = PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName];
+            var localeID = inputID + localeName;
+            bodyText += '<td>' + localeName + '</td>';
+            bodyText += '<td><input style="width:420px" class="configStringInput" type="text" value="' + value + '" id="' + localeID + '-input"></input></td>';
+            if (localeName != '') {
+                bodyText += '<td><span class="delete-row-icon action-icon pwm-icon pwm-icon-times" id="' + localeID + '-removeLocaleButton"></span></td>';
+            }
+            bodyText += '</tr><tr>';
+        }
+        bodyText += '</tr></table>';
+
+        PWM_MAIN.showDialog({
+            title: titleText,
+            text: bodyText,
+            okAction:function(){
+                finishAction();
+            },
+            loadFunction:function(){
+                for (var iter in PWM_VAR['clientSettingCache'][keyName][iteration][settingType]) {
+                    (function(localeName) {
+                        var localeID = inputID + localeName;
+                        PWM_MAIN.addEventHandler(localeID + '-input', 'input', function () {
+                            var inputElement = PWM_MAIN.getObject(localeID + '-input');
+                            var value = inputElement.value;
+                            PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName] = value;
+                            CustomLinkHandler.write(keyName);
+                        });
+                        PWM_MAIN.addEventHandler(localeID + '-removeLocaleButton', 'click', function () {
+                            delete PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName];
+                            CustomLinkHandler.write(keyName);
+                            CustomLinkHandler.multiLocaleStringDialog(keyName, iteration, settingType, finishAction, titleText);
+                        });
+                    }(iter));
+                }
+                UILibrary.addAddLocaleButtonRow(inputID + 'table', inputID, function(localeName){
+                    if (localeName in PWM_VAR['clientSettingCache'][keyName][iteration][settingType]) {
+                        alert('Locale is already present');
+                    } else {
+                        PWM_VAR['clientSettingCache'][keyName][iteration][settingType][localeName] = '';
+                        CustomLinkHandler.write(keyName);
+                        CustomLinkHandler.multiLocaleStringDialog(keyName, iteration, settingType, finishAction, titleText);
+                    }
+                }, Object.keys(PWM_VAR['clientSettingCache'][keyName][iteration][settingType]));
+            }
+        });
+    });
+};
+
+CustomLinkHandler.showSelectOptionsDialog = function(keyName, iteration) {
+    var inputID = 'value_' + keyName + '_' + iteration + "_" + "selectOptions_";
+    var bodyText = '';
+    bodyText += '<table class="noborder" id="' + inputID + 'table"">';
+    bodyText += '<tr>';
+    bodyText += '<td><b>Value</b></td><td><b>Display Name</b></td>';
+    bodyText += '</tr><tr>';
+    for (var optionName in PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions']) {
+        var value = PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions'][optionName];
+        var optionID = inputID + optionName;
+        bodyText += '<td>' + optionName + '</td><td>' + value + '</td>';
+        bodyText += '<td class="noborder" style="width:15px">';
+        bodyText += '<span id="' + optionID + '-removeButton" class="delete-row-icon action-icon pwm-icon pwm-icon-times"></span>';
+        bodyText += '</td>';
+        bodyText += '</tr><tr>';
+    }
+    bodyText += '</tr></table>';
+    bodyText += '<br/><br/><br/>';
+    bodyText += '<input class="configStringInput" style="width:200px" type="text" placeholder="Value" required id="addSelectOptionName"/>';
+    bodyText += '<input class="configStringInput" style="width:200px" type="text" placeholder="Display Name" required id="addSelectOptionValue"/>';
+    bodyText += '<button id="addSelectOptionButton"><span class="btn-icon pwm-icon pwm-icon-plus-square"/> Add</button>';
+
+    PWM_MAIN.showDialog({
+        title: 'Select Options for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'],
+        text: bodyText,
+        okAction: function(){
+            CustomLinkHandler.showOptionsDialog(keyName,iteration);
+        }
+    });
+
+    for (var optionName in PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions']) {
+        var loopID = inputID + optionName;
+        var optionID = inputID + optionName;
+        PWM_MAIN.clearDijitWidget(loopID);
+        PWM_MAIN.addEventHandler(optionID + '-removeButton','click',function(){
+            CustomLinkHandler.removeSelectOptionsOption(keyName,iteration,optionName);
+        });
+    }
+
+    PWM_MAIN.addEventHandler('addSelectOptionButton','click',function(){
+        var value = PWM_MAIN.getObject('addSelectOptionName').value;
+        var display = PWM_MAIN.getObject('addSelectOptionValue').value;
+        CustomLinkHandler.addSelectOptionsOption(keyName, iteration, value, display);
+    });
+};
+
+CustomLinkHandler.addSelectOptionsOption = function(keyName, iteration, optionName, optionValue) {
+    if (optionName == null || optionName.length < 1) {
+        alert('Name field is required');
+        return;
+    }
+
+    if (optionValue == null || optionValue.length < 1) {
+        alert('Value field is required');
+        return;
+    }
+
+    PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions'][optionName] = optionValue;
+    CustomLinkHandler.write(keyName);
+    CustomLinkHandler.showSelectOptionsDialog(keyName, iteration);
+};
+
+CustomLinkHandler.removeSelectOptionsOption = function(keyName, iteration, optionName) {
+    delete PWM_VAR['clientSettingCache'][keyName][iteration]['selectOptions'][optionName];
+    CustomLinkHandler.write(keyName);
+    CustomLinkHandler.showSelectOptionsDialog(keyName, iteration);
+};
+
+CustomLinkHandler.showDescriptionDialog = function(keyName, iteration) {
+    var finishAction = function(){ CustomLinkHandler.showOptionsDialog(keyName, iteration); };
+    var title = 'Description for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['description'][''];
+    CustomLinkHandler.multiLocaleStringDialog(keyName, iteration, 'description', finishAction, title);
+};
+

+ 4 - 0
src/main/webapp/public/resources/js/configeditor.js

@@ -936,6 +936,10 @@ PWM_CFGEDIT.initSettingDisplay = function(setting, options) {
             OptionListHandler.init(settingKey);
             break;
 
+        case 'CUSTOMLINKS':
+            CustomLinkHandler.init(settingKey, {});
+            break;
+
         case 'EMAIL':
             EmailTableHandler.init(settingKey);
             break;