From c5d415ef720932c5efc6b4838b23b5dbba6ba23d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hamza=20=C3=96zt=C3=BCrk?= Date: Mon, 22 Mar 2021 14:17:34 +0300 Subject: [PATCH 1/2] JDBCVCardProvider implementation. --- .../openfire/ldap/LdapVCardProvider.java | 133 +---- .../openfire/vcard/JDBCVCardProvider.java | 540 ++++++++++++++++++ .../jivesoftware/openfire/vcard/VCard.java | 92 +++ .../openfire/vcard/VCardTemplate.java | 52 ++ .../openfire/ldap/VCardTemplateTest.java | 25 +- .../jivesoftware/openfire/ldap/VCardTest.java | 56 +- 6 files changed, 728 insertions(+), 170 deletions(-) create mode 100644 xmppserver/src/main/java/org/jivesoftware/openfire/vcard/JDBCVCardProvider.java create mode 100644 xmppserver/src/main/java/org/jivesoftware/openfire/vcard/VCard.java create mode 100644 xmppserver/src/main/java/org/jivesoftware/openfire/vcard/VCardTemplate.java diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapVCardProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapVCardProvider.java index d4f4a06927..d069b94fe5 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapVCardProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapVCardProvider.java @@ -23,6 +23,8 @@ import org.jivesoftware.openfire.vcard.PhotoResizer; import org.jivesoftware.openfire.vcard.VCardManager; import org.jivesoftware.openfire.vcard.VCardProvider; +import org.jivesoftware.openfire.vcard.VCardTemplate; +import org.jivesoftware.openfire.vcard.VCard; import org.jivesoftware.util.Base64; import org.jivesoftware.util.*; import org.slf4j.Logger; @@ -33,8 +35,6 @@ import javax.naming.directory.DirContext; import javax.naming.ldap.Rdn; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * Read-only LDAP provider for vCards.Configuration consists of adding a provider: @@ -124,11 +124,6 @@ public class LdapVCardProvider implements VCardProvider, PropertyEventListener { */ private DefaultVCardProvider defaultProvider = null; - /** - * A regular expression that matches values enclosed in { and }, applying a group to the value that's surrounded. - */ - public static final Pattern PATTERN = Pattern.compile("(\\{)([\\d\\D&&[^}]]+)(})"); - public LdapVCardProvider() { // Convert XML based provider setup to Database based JiveGlobals.migrateProperty("ldap.vcard-mapping"); @@ -473,128 +468,4 @@ public void xmlPropertySet(String property, Map params) { public void xmlPropertyDeleted(String property, Map params) { //Ignore } - - /** - * Class to hold a Document representation of a vcard mapping - * and unique attribute placeholders. Used by VCard to apply - * a Map of ldap attributes to ldap values via - * MessageFormat - * - * @author rkelly - */ - static class VCardTemplate { - - private Document document; - - private String[] attributes; - - public VCardTemplate(Document document) { - Set set = new HashSet<>(); - this.document = document; - treeWalk(this.document.getRootElement(), set); - attributes = set.toArray(new String[0]); - } - - public String[] getAttributes() { - return attributes; - } - - public Document getDocument() { - return document; - } - - private void treeWalk(Element rootElement, Set set) { - for ( final Element element : rootElement.elements() ) { - final String value = element.getTextTrim(); - if ( value != null && !value.isEmpty()) { - final Matcher matcher = PATTERN.matcher(value); - while (matcher.find()) { - final String match = matcher.group(2); - Log.trace("Found attribute '{}'", match); - set.add(match); - } - } - treeWalk(element, set); - } - } - } - - /** - * vCard class that converts vcard data using a template. - */ - static class VCard { - - private VCardTemplate template; - - public VCard(VCardTemplate template) { - this.template = template; - } - - public Element getVCard(Map map) { - Document document = (Document) template.getDocument().clone(); - Element element = document.getRootElement(); - return treeWalk(element, map); - } - - private Element treeWalk(Element rootElement, Map map) { - for ( final Element element : rootElement.elements() ) { - String elementText = element.getTextTrim(); - if (elementText != null && !"".equals(elementText)) { - String format = element.getStringValue(); - - // A map that will hold all replacements for placeholders - final Map replacements = new HashMap<>(); - - // find all placeholders, and look up what they should be replaced with. - final Matcher matcher = PATTERN.matcher(format); - while (matcher.find()) { - final String group = matcher.group(); - final String attribute = matcher.group(2); - final String value = map.get(attribute); - replacements.put( group, value ); - } - - // perform the replacement. - for ( Map.Entry entry : replacements.entrySet() ) { - final String placeholder = entry.getKey(); - final String replacement = entry.getValue() != null ? entry.getValue() : ""; - format = format.replace(placeholder, replacement); - Log.trace("Replaced attribute '{}' with '{}'", placeholder, replacement); - } - - // When 'prioritized' replacements are used, the resulting value now will have those filled out: - // example: (|()(valueB)(valueC)) - // From this format, only the first non-empty value enclosed in brackets needs to be used. - final int start = format.indexOf("(|("); - final int end = format.indexOf("))"); - if ( start > -1 && end > start ) { - // Take the substring that is: (|()(valueB)(valueC)) - final String filter = format.substring(start, end + "))".length()); - - // Take the substring that is: )(valueB)(valueC - final String values = filter.substring("(|(".length(), filter.length() - "))".length() ); - - // Split on ")(" to get the individual values. - final String[] splitted = values.split("\\)\\("); - - // find the first non-empty string. - String firstValue = ""; - for ( final String split : splitted ) { - if ( split != null && !split.isEmpty() ) { - firstValue = split; - break; - } - } - - // Replace the original filter with just the first matching value. - format = format.replace(filter, firstValue); - } - - element.setText(format); - } - treeWalk(element, map); - } - return rootElement; - } - } } diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/vcard/JDBCVCardProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/vcard/JDBCVCardProvider.java new file mode 100644 index 0000000000..c908ce1f18 --- /dev/null +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/vcard/JDBCVCardProvider.java @@ -0,0 +1,540 @@ +package org.jivesoftware.openfire.vcard; + +import org.dom4j.Document; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.jivesoftware.database.DbConnectionManager; +import org.jivesoftware.openfire.user.UserNotFoundException; +import org.jivesoftware.util.*; +import org.jivesoftware.util.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xmpp.packet.JID; + +import java.sql.*; +import java.util.*; + +/** + * The JDBC user provider allows you to use an external database to define the vcards. + * All data is treated as read-only so any set operations will result in an exception. + *

To enable this provider, set the following in the system properties:

+ *
    + *
  • {@code provider.vcard.className = org.jivesoftware.openfire.vcard.JDBCVCardProvider}
  • + *
+ *

and an xml jdbc.vcard-mapping.

+ *

+ * Then you need to set your driver, connection string and SQL statements: + *

+ *
    + *
  • {@code jdbcProvider.driver = com.mysql.jdbc.Driver}
  • + *
  • {@code jdbcProvider.connectionString = jdbc:mysql://localhost/dbname?user=username&password=secret}
  • + *
  • {@code jdbcUserProvider.loadVCardSQL = SELECT name, email FROM myUser WHERE user = ?}
  • + *
+ * + * In order to use the configured JDBC connection provider do not use a JDBC + * connection string, set the following property + * + *
    + *
  • {@code jdbcUserProvider.useConnectionProvider = true}
  • + *
+ * + *

+ * The vcard attributes can be configured by adding an attrs="attr1,attr2" + * attribute to the vcard elements.

+ *

+ * Arbitrary text can be used for the element values as well as MessageFormat + * style placeholders for the jdbc attributes. For example, if you wanted to map the JDBC + * attribute displayName to the vcard element FN, the xml + * nippet would be:


{0}
+ *

+ * The vCard XML must be escaped in CDATA and must also be well formed. It is the exact + * XML this provider will send to a client after after stripping attr attributes + * and populating the placeholders with the data retrieved from JDBC. This system should + * be flexible enough to handle any client's vCard format. An example mapping follows.
+ *

+ * {@code jdbc.vcard-mapping = + * + * {0} + * {0} + * {0} + * + * + * Ste 500 + * 317 SW Alder St + * Portland + * Oregon + * 97204 + * USA + * + * + * + * + * {0} + * + * + * + * {0} + * + * {0} + * {0} + * + * {0} + * {0} + * + * {0} + * + * uid: {0} home: {1} shell: {2} + * + * + * ]]> + * } + *

+ * An easy way to get the vcard format your client needs, assuming you've been + * using the database store, is to do a SELECT value FROM ofVCard WHERE + * username='some_user' in your favorite sql querier and paste the result + * into the vcard-mapping (don't forget the CDATA).

+ * + * @author hamzaozturk + */ +public class JDBCVCardProvider implements VCardProvider, PropertyEventListener { + + public static final SystemProperty STORE_AVATAR_IN_DB = SystemProperty.Builder.ofType(Boolean.class) + .setKey("jdbc.override.avatar") + .setDefaultValue(false) + .setDynamic(true) + .addListener(JDBCVCardProvider::setDbStorageEnabled) + .build(); + + private static final Logger Log = LoggerFactory.getLogger(JDBCVCardProvider.class); + + private String connectionString; + + private String loadVCardSQL; + private boolean useConnectionProvider; + + private VCardTemplate template; + private static boolean dbStorageEnabled = false; + + /** + * The default vCard provider is used to handle the vCard in the database. vCard + * fields that can be overriden are stored in the database. + * + * This is used/created only if we are storing avatars in the database. + */ + private DefaultVCardProvider defaultProvider = null; + + /** + * Constructs a new JDBC vcard provider. + */ + public JDBCVCardProvider() { + // Convert XML based provider setup to Database based + JiveGlobals.migrateProperty("jdbcProvider.driver"); + JiveGlobals.migrateProperty("jdbcProvider.connectionString"); + JiveGlobals.migrateProperty("jdbcVCardProvider.loadVCardSQL"); + JiveGlobals.migrateProperty("jdbc.vcard-mapping"); + + useConnectionProvider = JiveGlobals.getBooleanProperty("jdbcVCardProvider.useConnectionProvider"); + + // Load the JDBC driver and connection string. + if (!useConnectionProvider) { + String jdbcDriver = JiveGlobals.getProperty("jdbcProvider.driver"); + try { + Class.forName(jdbcDriver).newInstance(); + } + catch (Exception e) { + Log.error("Unable to load JDBC driver: " + jdbcDriver, e); + return; + } + connectionString = JiveGlobals.getProperty("jdbcProvider.connectionString"); + } + + // Load database statements for vcard data. + loadVCardSQL = JiveGlobals.getProperty("jdbcVCardProvider.loadVCardSQL"); + + initTemplate(); + // Listen to property events so that the template is always up to date + PropertyEventDispatcher.addListener(this); + // DB vcard provider used for loading properties overwritten in the DB + defaultProvider = new DefaultVCardProvider(); + // Check of avatars can be overwritten (and stored in the database) + setDbStorageEnabled(STORE_AVATAR_IN_DB.getValue()); + } + + private static void setDbStorageEnabled(final boolean value) { + dbStorageEnabled = value; + } + + /** + * Initializes the VCard template as set by the administrator. + */ + private void initTemplate() { + String property = JiveGlobals.getProperty("jdbc.vcard-mapping"); + Log.debug("JDBCVCardProvider: Found vcard mapping: '" + property); + try { + // Remove CDATA wrapping element + if (property.startsWith(" getJDBCAttributes(String username) { + // OF-1837: When the database does not hold escaped data, our query should use unescaped values in the 'where' clause. + final String queryValue = assumePersistedDataIsEscaped() ? username : JID.unescapeNode( username ); + + Connection con = null; + PreparedStatement pstmt = null; + ResultSet rs = null; + Map map = new HashMap<>(); + try { + con = getConnection(); + pstmt = con.prepareStatement(loadVCardSQL); + pstmt.setString(1, queryValue); + rs = pstmt.executeQuery(); + if (!rs.next()) { + throw new UserNotFoundException(); + } + + for (String attribute : template.getAttributes()) { + Object ob = rs.getObject(attribute); + String value; + if (ob == null) { + Log.debug("JDBCVCardProvider: No jdbc value found for attribute '" + attribute + "'"); + value = ""; + } else { + Log.debug("JDBCVCardProvider: Found attribute "+attribute+" of type: "+ob.getClass()); + if(ob instanceof String) { + value = (String)ob; + } else { + value = Base64.encodeBytes((byte[])ob); + } + } + Log.debug("JDBCVCardProvider: Jdbc attribute '" + attribute + "'=>'" + value + "'"); + map.put(attribute, value); + } + } + catch (Exception e) { + Log.error(e.getMessage(), e); + } + finally { + DbConnectionManager.closeConnection(rs, pstmt, con); + } + + return map; + } + + /** + * Loads the avatar from JDBC, based off the vcard template. + * + * If enabled, will replace a blank PHOTO element with one from a DB stored vcard. + * + * @param username User we are loading the vcard for. + * @return The loaded vcard element, or null if none found. + */ + @Override + public Element loadVCard(String username) { + Map map = getJDBCAttributes(username); + Log.debug("JDBCVCardProvider: Getting mapped vcard for " + username); + Element vcard = new VCard(template).getVCard(map); + // If we have a vcard from jdbc, but it doesn't have an avatar filled in, then we + // may fill it with a locally stored vcard element. + if (dbStorageEnabled && vcard != null && (vcard.element("PHOTO") == null || vcard.element("PHOTO").element("BINVAL") == null || vcard.element("PHOTO").element("BINVAL").getText().matches("\\s*"))) { + Element avatarElement = loadAvatarFromDatabase(username); + if (avatarElement != null) { + Log.debug("JDBCVCardProvider: Adding avatar element from local storage"); + Element currentElement = vcard.element("PHOTO"); + if (currentElement != null) { + vcard.remove(currentElement); + } + vcard.add(avatarElement); + } + } + + if ( JiveGlobals.getBooleanProperty( PhotoResizer.PROPERTY_RESIZE_ON_LOAD, PhotoResizer.PROPERTY_RESIZE_ON_LOAD_DEFAULT ) ) + { + PhotoResizer.resizeAvatar( vcard ); + } + + Log.debug("JDBCVCardProvider: Returning vcard"); + return vcard; + } + + /** + * Loads the avatar element from the user's DB stored vcard. + * + * @param username User whose vcard/avatar element we are loading. + * @return Loaded avatar element or null if not found. + */ + private Element loadAvatarFromDatabase(String username) { + Element vcardElement = defaultProvider.loadVCard(username); + Element avatarElement = null; + if (vcardElement != null && vcardElement.element("PHOTO") != null) { + avatarElement = vcardElement.element("PHOTO").createCopy(); + } + return avatarElement; + } + + /** + * Handles when a user creates a new vcard. + * + * @param username User that created a new vcard. + * @param vCardElement vCard element containing the new vcard. + * @throws UnsupportedOperationException If an invalid field is changed or we are in readonly mode. + */ + @Override + public Element createVCard(String username, Element vCardElement) throws AlreadyExistsException { + // Reject the operation since the provider is read-only + throw new UnsupportedOperationException(); + } + + /** + * Handles when a user updates their vcard. + * + * @param username User that updated their vcard. + * @param vCardElement vCard element containing the new vcard. + * @throws UnsupportedOperationException If an invalid field is changed or we are in readonly mode. + */ + @Override + public Element updateVCard(String username, Element vCardElement) throws UnsupportedOperationException { + if (dbStorageEnabled && defaultProvider != null) { + if (isValidVCardChange(username, vCardElement)) { + Element mergedVCard = getMergedVCard(username, vCardElement); + try { + defaultProvider.updateVCard(username, mergedVCard); + } catch (NotFoundException e) { + try { + defaultProvider.createVCard(username, mergedVCard); + } catch (AlreadyExistsException e1) { + // Ignore + } + } + return mergedVCard; + } + else { + throw new UnsupportedOperationException("JDBCVCardProvider: Invalid vcard changes."); + } + } + else { + throw new UnsupportedOperationException("JDBCVCardProvider: VCard changes not allowed."); + } + } + + /** + * Delets a user vcard. This method should throw an UnsupportedOperationException + * if this operation is not supported by the backend vcard store. + * + * @param username the username to delete. + * @throws UnsupportedOperationException if the provider does not support the + * operation. + */ + @Override + public void deleteVCard(String username) { + // Reject the operation since the provider is read-only + throw new UnsupportedOperationException(); + } + + /** + * Returns true or false if the change to the existing vcard is valid (only to PHOTO element) + * + * @param username User who's JDBC-based vcard we will compare with. + * @param newvCard New vCard Element we will compare against. + * @return True or false if the changes made were valid (only to PHOTO element) + */ + private Boolean isValidVCardChange(String username, Element newvCard) { + if (newvCard == null) { + // Well if there's nothing to change, of course it's valid. + Log.debug("JDBCVCardProvider: No new vcard provided (no changes), accepting."); + return true; + } + Map map = getJDBCAttributes(username); + // Retrieve JDBC created vcard for comparison + Element jdbcvCard = new VCard(template).getVCard(map); + if (jdbcvCard == null) { + // This person has no vcard at all, may not change it! + Log.debug("JDBCVCardProvider: User has no JDBC vcard, nothing they can change, rejecting."); + return false; + } + // If the JDBC vcard has a non-empty PHOTO element set, then there is literally no way this will be accepted. + Element jdbcPhotoElem = jdbcvCard.element("PHOTO"); + if (jdbcPhotoElem != null) { + Element jdbcBinvalElem = jdbcPhotoElem.element("BINVAL"); + if (jdbcBinvalElem != null && !jdbcBinvalElem.getTextTrim().matches("\\s*")) { + // JDBC is providing a valid PHOTO element, byebye! + Log.debug("JDBCVCardProvider: JDBC has a PHOTO element set, no way to override, rejecting."); + return false; + } + } + // Retrieve database vcard, if it exists + Element dbvCard = defaultProvider.loadVCard(username); + if (dbvCard != null) { + Element dbPhotoElem = dbvCard.element("PHOTO"); + if (dbPhotoElem == null) { + // DB has no photo, lets accept what we got. + Log.debug("JDBCVCardProvider: Database has no PHOTO element, accepting update."); + return true; + } + else { + Element newPhotoElem = newvCard.element("PHOTO"); + if (newPhotoElem == null) { + Log.debug("JDBCVCardProvider: Photo element was removed, accepting update."); + return true; + } + // Note: NodeComparator never seems to consider these equal, even if they are? + if (!dbPhotoElem.asXML().equals(newPhotoElem.asXML())) { + // Photo element was changed. Ignore all other changes and accept this. + Log.debug("JDBCVCardProvider: PHOTO element changed, accepting update."); + return true; + } + } + } + else { + // No vcard exists in database + Log.debug("JDBCVCardProvider: Database has no vCard stored, accepting update."); + return true; + } + // Ok, either something bad changed or nothing changed. Either way, user either: + // 1. should not have tried to change something 'readonly' + // 2. shouldn't have bothered submitting no changes + // So we'll consider this a bad return. + Log.debug("JDBCVCardProvider: PHOTO element didn't change, no reason to accept this, rejecting."); + return false; + } + + /** + * Returns a merged JDBC vCard combined with a PHOTO element provided in specified vCard. + * + * @param username User whose vCard this is. + * @param mergeVCard vCard element that we are merging PHOTO element from into the JDBC vCard. + * @return vCard element after merging in PHOTO element to JDBC data. + */ + private Element getMergedVCard(String username, Element mergeVCard) { + Map map = getJDBCAttributes(username); + Log.debug("JDBCVCardProvider: Retrieving JDBC mapped vcard for " + username); + if (map.isEmpty()) { + return null; + } + Element vcard = new VCard(template).getVCard(map); + if (mergeVCard == null) { + // No vcard passed in? Hrm. Fine, return JDBC vcard. + return vcard; + } + if (mergeVCard.element("PHOTO") == null) { + // Merged vcard has no photo element, return JDBC vcard as is. + return vcard; + } + Element photoElement = mergeVCard.element("PHOTO").createCopy(); + if (photoElement == null || photoElement.element("BINVAL") == null || photoElement.element("BINVAL").getText().matches("\\s*")) { + // We were passed something null or empty, so lets just return the JDBC based vcard. + return vcard; + } + // Now we need to check that the JDBC vcard doesn't have a PHOTO element that's filled in. + if (!((vcard.element("PHOTO") == null || vcard.element("PHOTO").element("BINVAL") == null || vcard.element("PHOTO").element("BINVAL").getText().matches("\\s*")))) { + // Hrm, it does, return the original vcard; + return vcard; + } + Log.debug("JDBCVCardProvider: Merging avatar element from passed vcard"); + Element currentElement = vcard.element("PHOTO"); + if (currentElement != null) { + vcard.remove(currentElement); + } + vcard.add(photoElement); + return vcard; + } + + /** + * Returns true if this VCardProvider is read-only. When read-only, + * vcards can not be created, deleted, or modified. + * + * @return true if the vcard provider is read-only. + */ + @Override + public boolean isReadOnly() { + return !dbStorageEnabled; + } + + /** + * A property was set. The parameter map {@code params} will contain the + * the value of the property under the key {@code value}. + * + * @param property the name of the property. + * @param params event parameters. + */ + @Override + public void propertySet(String property, Map params) { + if ("jdbc.vcard-mapping".equals(property)) { + initTemplate(); + // Reset cache of vCards + VCardManager.getInstance().reset(); + } + } + + /** + * A property was deleted. + * + * @param property the name of the property deleted. + * @param params event parameters. + */ + @Override + public void propertyDeleted(String property, Map params) { + //Ignore + } + + /** + * An XML property was set. The parameter map {@code params} will contain the + * the value of the property under the key {@code value}. + * + * @param property the name of the property. + * @param params event parameters. + */ + @Override + public void xmlPropertySet(String property, Map params) { + //Ignore + } + + /** + * An XML property was deleted. + * + * @param property the name of the property. + * @param params event parameters. + */ + @Override + public void xmlPropertyDeleted(String property, Map params) { + //Ignore + } +} diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/vcard/VCard.java b/xmppserver/src/main/java/org/jivesoftware/openfire/vcard/VCard.java new file mode 100644 index 0000000000..778b0fec7f --- /dev/null +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/vcard/VCard.java @@ -0,0 +1,92 @@ +package org.jivesoftware.openfire.vcard; + +import org.dom4j.Document; +import org.dom4j.Element; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * vCard class that converts vcard data using a template. + */ +public class VCard { + + /** + * A regular expression that matches values enclosed in { and }, applying a group to the value that's surrounded. + */ + public static final Pattern PATTERN = Pattern.compile("(\\{)([\\d\\D&&[^}]]+)(})"); + + private VCardTemplate template; + + public VCard(VCardTemplate template) { + this.template = template; + } + + public Element getVCard(Map map) { + Document document = (Document) template.getDocument().clone(); + Element element = document.getRootElement(); + return treeWalk(element, map); + } + + private Element treeWalk(Element rootElement, Map map) { + for ( final Element element : rootElement.elements() ) { + String elementText = element.getTextTrim(); + if (elementText != null && !"".equals(elementText)) { + String format = element.getStringValue(); + + // A map that will hold all replacements for placeholders + final Map replacements = new HashMap<>(); + + // find all placeholders, and look up what they should be replaced with. + final Matcher matcher = PATTERN.matcher(format); + while (matcher.find()) { + final String group = matcher.group(); + final String attribute = matcher.group(2); + final String value = map.get(attribute); + replacements.put( group, value ); + } + + // perform the replacement. + for ( Map.Entry entry : replacements.entrySet() ) { + final String placeholder = entry.getKey(); + final String replacement = entry.getValue() != null ? entry.getValue() : ""; + format = format.replace(placeholder, replacement); + } + + // When 'prioritized' replacements are used, the resulting value now will have those filled out: + // example: (|()(valueB)(valueC)) + // From this format, only the first non-empty value enclosed in brackets needs to be used. + final int start = format.indexOf("(|("); + final int end = format.indexOf("))"); + if ( start > -1 && end > start ) { + // Take the substring that is: (|()(valueB)(valueC)) + final String filter = format.substring(start, end + "))".length()); + + // Take the substring that is: )(valueB)(valueC + final String values = filter.substring("(|(".length(), filter.length() - "))".length() ); + + // Split on ")(" to get the individual values. + final String[] splitted = values.split("\\)\\("); + + // find the first non-empty string. + String firstValue = ""; + for ( final String split : splitted ) { + if ( split != null && !split.isEmpty() ) { + firstValue = split; + break; + } + } + + // Replace the original filter with just the first matching value. + format = format.replace(filter, firstValue); + } + + element.setText(format); + } + treeWalk(element, map); + } + return rootElement; + } +} diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/vcard/VCardTemplate.java b/xmppserver/src/main/java/org/jivesoftware/openfire/vcard/VCardTemplate.java new file mode 100644 index 0000000000..117c67335b --- /dev/null +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/vcard/VCardTemplate.java @@ -0,0 +1,52 @@ +package org.jivesoftware.openfire.vcard; + +import org.dom4j.Document; +import org.dom4j.Element; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; + +/** + * Class to hold a Document representation of a vcard mapping + * and unique attribute placeholders. Used by VCard to apply + * a Map of ldap attributes to ldap values via + * MessageFormat + * + * @author rkelly + */ +public class VCardTemplate { + + private Document document; + + private String[] attributes; + + public VCardTemplate(Document document) { + Set set = new HashSet<>(); + this.document = document; + treeWalk(this.document.getRootElement(), set); + attributes = set.toArray(new String[0]); + } + + public String[] getAttributes() { + return attributes; + } + + public Document getDocument() { + return document; + } + + private void treeWalk(Element rootElement, Set set) { + for ( final Element element : rootElement.elements() ) { + final String value = element.getTextTrim(); + if ( value != null && !value.isEmpty()) { + final Matcher matcher = VCard.PATTERN.matcher(value); + while (matcher.find()) { + final String match = matcher.group(2); + set.add(match); + } + } + treeWalk(element, set); + } + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/ldap/VCardTemplateTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/ldap/VCardTemplateTest.java index 17d6fbcff8..3d276c10ca 100644 --- a/xmppserver/src/test/java/org/jivesoftware/openfire/ldap/VCardTemplateTest.java +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/ldap/VCardTemplateTest.java @@ -18,6 +18,7 @@ import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.io.SAXReader; +import org.jivesoftware.openfire.vcard.VCardTemplate; import org.jivesoftware.util.CertificateManager; import org.junit.Test; @@ -29,7 +30,7 @@ import static org.junit.Assert.*; /** - * Unit tests that verify the implementation of {@link org.jivesoftware.openfire.ldap.LdapVCardProvider.VCardTemplate} + * Unit tests that verify the implementation of {@link org.jivesoftware.openfire.vcard.VCardTemplate} * * @author Guus der Kinderen, guus.der.kinderen@gmail.com */ @@ -45,7 +46,7 @@ public void testIdentifyPlaceholder() throws Exception final Document input = DocumentHelper.parseText("{placeholder}"); // Execute system under test. - final LdapVCardProvider.VCardTemplate result = new LdapVCardProvider.VCardTemplate(input); + final VCardTemplate result = new VCardTemplate(input); // Verify result. assertNotNull( result ); @@ -65,7 +66,7 @@ public void testIdentifyEmbeddedPlaceholder() throws Exception final Document input = DocumentHelper.parseText("foo{placeholder}bar"); // Execute system under test. - final LdapVCardProvider.VCardTemplate result = new LdapVCardProvider.VCardTemplate(input); + final VCardTemplate result = new VCardTemplate(input); // Verify result. assertNotNull( result ); @@ -85,7 +86,7 @@ public void testIdentifyEmbeddedPlaceholderVariant2() throws Exception final Document input = DocumentHelper.parseText("foo, {placeholder} bar"); // Execute system under test. - final LdapVCardProvider.VCardTemplate result = new LdapVCardProvider.VCardTemplate(input); + final VCardTemplate result = new VCardTemplate(input); // Verify result. assertNotNull( result ); @@ -105,7 +106,7 @@ public void testIdentifyPlaceholders() throws Exception final Document input = DocumentHelper.parseText("{placeholderA}, {placeholderB}"); // Execute system under test. - final LdapVCardProvider.VCardTemplate result = new LdapVCardProvider.VCardTemplate(input); + final VCardTemplate result = new VCardTemplate(input); // Verify result. assertNotNull( result ); @@ -126,7 +127,7 @@ public void testIdentifyEmbeddedPlaceholders() throws Exception final Document input = DocumentHelper.parseText("foo {placeholderA}, {placeholderB} bar"); // Execute system under test. - final LdapVCardProvider.VCardTemplate result = new LdapVCardProvider.VCardTemplate(input); + final VCardTemplate result = new VCardTemplate(input); // Verify result. assertNotNull( result ); @@ -149,7 +150,7 @@ public void testIdentifyPrioritizedPlaceholders() throws Exception final Document input = DocumentHelper.parseText("(|({placeholderA})({placeholderB})({placeholderC}))"); // Execute system under test. - final LdapVCardProvider.VCardTemplate result = new LdapVCardProvider.VCardTemplate(input); + final VCardTemplate result = new VCardTemplate(input); // Verify result. assertNotNull( result ); @@ -170,7 +171,7 @@ public void testIdentifyNonPlaceholder() throws Exception final Document input = DocumentHelper.parseText("placeholder"); // Execute system under test. - final LdapVCardProvider.VCardTemplate result = new LdapVCardProvider.VCardTemplate(input); + final VCardTemplate result = new VCardTemplate(input); // Verify result. assertNotNull( result ); @@ -190,7 +191,7 @@ public void testIdentifyNonPlaceholderWithSeparatorChar() throws Exception final Document input = DocumentHelper.parseText("placeholder/me"); // Execute system under test. - final LdapVCardProvider.VCardTemplate result = new LdapVCardProvider.VCardTemplate(input); + final VCardTemplate result = new VCardTemplate(input); // Verify result. assertNotNull( result ); @@ -212,7 +213,7 @@ public void testDefaultTemplate() throws Exception } // Execute system under test. - final LdapVCardProvider.VCardTemplate result = new LdapVCardProvider.VCardTemplate(input); + final VCardTemplate result = new VCardTemplate(input); // Verify result. assertNotNull( result ); @@ -251,7 +252,7 @@ public void testCombinedTemplate() throws Exception } // Execute system under test. - final LdapVCardProvider.VCardTemplate result = new LdapVCardProvider.VCardTemplate(input); + final VCardTemplate result = new VCardTemplate(input); // Verify result. assertNotNull( result ); @@ -294,7 +295,7 @@ public void testPrioritizedTemplate() throws Exception } // Execute system under test. - final LdapVCardProvider.VCardTemplate result = new LdapVCardProvider.VCardTemplate(input); + final VCardTemplate result = new VCardTemplate(input); // Verify result. assertNotNull( result ); diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/ldap/VCardTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/ldap/VCardTest.java index 0155af66ce..ffa0ebe966 100644 --- a/xmppserver/src/test/java/org/jivesoftware/openfire/ldap/VCardTest.java +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/ldap/VCardTest.java @@ -18,6 +18,8 @@ import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; +import org.jivesoftware.openfire.vcard.VCard; +import org.jivesoftware.openfire.vcard.VCardTemplate; import org.junit.Test; import java.util.Arrays; @@ -28,7 +30,7 @@ import static org.junit.Assert.assertTrue; /** - * Unit tests that verify the implementation of {@link LdapVCardProvider.VCard} + * Unit tests that verify the implementation of {@link VCard} * * @author Guus der Kinderen, guus.der.kinderen@gmail.com */ @@ -42,12 +44,12 @@ public void testReplacePlaceholder() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("{placeholder}"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("placeholder", "value"); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -64,12 +66,12 @@ public void testReplaceEmbeddedPlaceholder() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("foo{placeholder}bar"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("placeholder", "value"); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -86,12 +88,12 @@ public void testReplaceEmbeddedPlaceholderVariant2() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("foo, {placeholder} bar"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("placeholder", "value"); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -108,13 +110,13 @@ public void testReplacePlaceholders() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("{placeholderA}, {placeholderB}"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("placeholderA", "valueA"); attributes.put("placeholderB", "valueB"); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -133,14 +135,14 @@ public void testReplacePrioritizedPlaceholders() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("(|({placeholderA})({placeholderB})({placeholderC}))"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("placeholderA", ""); attributes.put("placeholderB", "valueB"); attributes.put("placeholderC", "valueC"); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -159,14 +161,14 @@ public void testReplacePrioritizedPlaceholdersVariantA() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("(|({placeholderA})({placeholderB})({placeholderC}))"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("placeholderA", "valueA"); attributes.put("placeholderB", "valueB"); attributes.put("placeholderC", "valueC"); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -185,14 +187,14 @@ public void testReplacePrioritizedPlaceholdersVariantB() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("(|({placeholderA})({placeholderB})({placeholderC}))"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("placeholderA", "valueA"); attributes.put("placeholderB", ""); attributes.put("placeholderC", ""); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -211,14 +213,14 @@ public void testReplacePrioritizedPlaceholdersVariantC() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("(|({placeholderA})({placeholderB})({placeholderC}))"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("placeholderA", ""); attributes.put("placeholderB", ""); attributes.put("placeholderC", "valueC"); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -237,14 +239,14 @@ public void testReplacePrioritizedPlaceholdersVariantD() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("(|({placeholderA})({placeholderB})({placeholderC}))"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("placeholderA", ""); attributes.put("placeholderB", ""); attributes.put("placeholderC", ""); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -261,13 +263,13 @@ public void testReplaceEmbeddedPlaceholders() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("foo {placeholderA}, {placeholderB} bar"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("placeholderA", "valueA"); attributes.put("placeholderB", "valueB"); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -283,12 +285,12 @@ public void testDontReplaceNonPlaceholder() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("placeholder"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("placeholder", "value"); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -307,11 +309,11 @@ public void testReplaceMissingMappedValue() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("{placeholder}"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. @@ -331,13 +333,13 @@ public void testIdentifyNonPlaceholderWithSeparatorChar() throws Exception { // Setup fixture. final Document doc = DocumentHelper.parseText("place/holder"); - final LdapVCardProvider.VCardTemplate template = new LdapVCardProvider.VCardTemplate(doc); + final VCardTemplate template = new VCardTemplate(doc); final Map attributes = new HashMap<>(); attributes.put("place", "value"); attributes.put("holder", "value"); // Execute system under test. - final LdapVCardProvider.VCard vCard = new LdapVCardProvider.VCard(template); + final VCard vCard = new VCard(template); final Element result = vCard.getVCard(attributes); // Verify result. From 57bf7fd8c06c6eaab540cdf7309b65e79e5cd39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hamza=20=C3=96zt=C3=BCrk?= Date: Mon, 22 Mar 2021 15:29:38 +0300 Subject: [PATCH 2/2] Add missing property descriptions. --- i18n/src/main/resources/openfire_i18n.properties | 1 + i18n/src/main/resources/openfire_i18n_de.properties | 1 + i18n/src/main/resources/openfire_i18n_es.properties | 1 + 3 files changed, 3 insertions(+) diff --git a/i18n/src/main/resources/openfire_i18n.properties b/i18n/src/main/resources/openfire_i18n.properties index 7bf4beca82..f496731093 100644 --- a/i18n/src/main/resources/openfire_i18n.properties +++ b/i18n/src/main/resources/openfire_i18n.properties @@ -1647,6 +1647,7 @@ system_property.xmpp.client.version-query.delay=After this amount of time has pa system_property.xmpp.server.rewrite.replace-missing-to=If the server receives a message or IQ stanza with no 'to' attribute, set the 'to' attribute to the bare JID representation of the 'from' attribute value. system_property.cluster-monitor.service-enabled=Set to true to send messages to admins on cluster events, otherwise false system_property.ldap.override.avatar=Set to true to save avatars in the local database, otherwise false +system_property.jdbc.override.avatar=Set to true to save avatars in the local database, otherwise false system_property.xmpp.domain=The XMPP domain of this server. Do not change this property directly, instead re-run the setup process. system_property.plugins.upload.magic-number.values.enabled=Enables verification of 'magic bytes' when processing uploaded data that is expected to be an Openfire plugin. system_property.plugins.upload.magic-number.values.expected-value=A list of hexadecimal representations of 'magic byte' sequences that can be used to identify an uploaded plugin file. diff --git a/i18n/src/main/resources/openfire_i18n_de.properties b/i18n/src/main/resources/openfire_i18n_de.properties index 57739b6038..b23283ad8a 100644 --- a/i18n/src/main/resources/openfire_i18n_de.properties +++ b/i18n/src/main/resources/openfire_i18n_de.properties @@ -1187,6 +1187,7 @@ system_property.xmpp.client.idle.ping=Auf true setzen, um inaktive Clients anzup system_property.cluster-monitor.service-enabled=Auf true setzen, um Nachrichten an Administratoren bei \ Cluster-Ereignissen zu senden, ansonsten false. system_property.ldap.override.avatar=Auf true setzen, um Avatare in der lokalen Datenbank zu speichern, ansonsten false. +system_property.jdbc.override.avatar=Auf true setzen, um Avatare in der lokalen Datenbank zu speichern, ansonsten false. system_property.xmpp.domain=Die XMPP-Domäne dieses Servers. Ändern Sie diese Eigenschaft nicht direkt, \ sondern führen Sie den Setup-Prozess erneut aus. system_property.provider.admin.className=Die Klasse, die verwendet werden soll, um Openfire-Administratoren zu \ diff --git a/i18n/src/main/resources/openfire_i18n_es.properties b/i18n/src/main/resources/openfire_i18n_es.properties index cd93ca0f75..e8f8fc56ec 100644 --- a/i18n/src/main/resources/openfire_i18n_es.properties +++ b/i18n/src/main/resources/openfire_i18n_es.properties @@ -1161,6 +1161,7 @@ system_property.xmpp.client.idle=Tiempo en milisegundos hasta que las sesiones i system_property.xmpp.client.idle.ping=Configurar en true para hacer ping a los clientes, en otro caso en false system_property.cluster-monitor.service-enabled=Configurar en true para enviar mensajes a los administradores en caso de ocurrir eventos con el cluster, en otro caso false system_property.ldap.override.avatar=Configurar en true para guardar avatares en la base de datos local, en otro caso false +system_property.jdbc.override.avatar=Configurar en true para guardar avatares en la base de datos local, en otro caso false system_property.xmpp.domain=El dominio XMPP de este servidor. No cambie esta propiedad directamente, en su lugar ejecute nuevamente el proceso de instalación. system_property.provider.admin.className=La clase a usar para autorizar administradores de Openfire system_property.provider.group.className=La clase a usar para determinar los grupos de los usuarios de Openfire