diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index b5669b6af77d93e18aa7599a102a69435cee29b5..f559bf78e49b4f067e6166d4ac0a00e4e6bcda91 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -14,7 +14,6 @@ import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.data.Data; import im.conversations.android.xmpp.model.data.Field; import im.conversations.android.xmpp.model.disco.info.InfoQuery; @@ -158,8 +157,7 @@ public class MucOptions { final var serviceDiscoveryResult = getServiceDiscoveryResult(); return serviceDiscoveryResult == null ? null - : serviceDiscoveryResult.getServiceDiscoveryExtension( - "http://jabber.org/protocol/muc#roominfo"); + : serviceDiscoveryResult.getServiceDiscoveryExtension(Namespace.MUC_ROOM_INFO); } public String getAvatar() { @@ -857,7 +855,7 @@ public class MucOptions { private Jid realJid; private Jid fullJid; private long pgpKeyId = 0; - private Avatar avatar; + private String avatar; private final MucOptions options; private ChatState chatState = Config.DEFAULT_CHAT_STATE; private String occupantId; @@ -913,7 +911,7 @@ public class MucOptions { } } - public boolean setAvatar(final Avatar avatar) { + public boolean setAvatar(final String avatar) { if (this.avatar != null && this.avatar.equals(avatar)) { return false; } else { @@ -930,7 +928,7 @@ public class MucOptions { getContact(); if (avatar != null) { - return avatar.getFilename(); + return avatar; } if (realJid == null) { return null; diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 3a4fe249444650a68539c48ae63ea7e7e2600f5c..553062e856df3b30bcac5fa8fdeca02349e839f4 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -13,7 +13,6 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.stanza.Iq; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; @@ -72,21 +71,6 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public Iq retrievePepAvatar(final Avatar avatar) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final var packet = retrieve(Namespace.AVATAR_DATA, item); - packet.setTo(avatar.owner); - return packet; - } - - public Iq retrieveVcardAvatar(final Avatar avatar) { - final Iq packet = new Iq(Iq.Type.GET); - packet.setTo(avatar.owner); - packet.addChild("vCard", "vcard-temp"); - return packet; - } - public Iq retrieveDeviceIds(final Jid to) { final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); if (to != null) { diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 4e73be5f880507a7dff46702e82174cea32be4c2..adfede484d16fa25b62a570f3061794701968622 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -23,12 +23,13 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.manager.AvatarManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; import eu.siacs.conversations.xmpp.manager.RosterManager; -import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.model.occupant.OccupantId; +import im.conversations.android.xmpp.model.vcard.update.VCardUpdate; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeoutException; @@ -81,7 +82,7 @@ public class PresenceParser extends AbstractParser if (!from.isBareJid()) { final String type = packet.getAttribute("type"); final Element x = packet.findChild("x", Namespace.MUC_USER); - Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); + final var vCardUpdate = packet.getExtension(VCardUpdate.class); final List codes = getStatusCodes(x); if (type == null) { if (x != null) { @@ -162,32 +163,8 @@ public class PresenceParser extends AbstractParser } } } - if (avatar != null) { - avatar.owner = from; - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { - if (user.setAvatar(avatar)) { - mXmppConnectionService.getAvatarService().clear(user); - } - - // TODO don’t do that. This will just overwrite (better) PEP avatars - - if (user.getRealJid() != null) { - final Contact c = - conversation - .getAccount() - .getRoster() - .getContact(user.getRealJid()); - if (c.setAvatar(avatar.sha1sum)) { - connection - .getManager(RosterManager.class) - .writeToDatabaseAsync(); - mXmppConnectionService.getAvatarService().clear(c); - } - mXmppConnectionService.updateRosterUi(); - } - } else if (mXmppConnectionService.isDataSaverDisabled()) { - mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar); - } + if (vCardUpdate != null) { + getManager(AvatarManager.class).handleVCardUpdate(from, vCardUpdate); } } } @@ -346,33 +323,6 @@ public class PresenceParser extends AbstractParser if (type == null) { final String resource = from.isBareJid() ? "" : from.getResource(); - // TODO simply don’t parse avatars for contacts at all. Only if presence is bare and a - // MUC - - final Avatar avatar = - Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); - if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) { - avatar.owner = from.asBareJid(); - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { - if (avatar.owner.equals(account.getJid().asBareJid())) { - account.setAvatar(avatar.getFilename()); - mXmppConnectionService.databaseBackend.updateAccount(account); - mXmppConnectionService.getAvatarService().clear(account); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateAccountUi(); - } else { - if (contact.setAvatar(avatar.sha1sum)) { - connection.getManager(RosterManager.class).writeToDatabaseAsync(); - mXmppConnectionService.getAvatarService().clear(contact); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateRosterUi(); - } - } - } else if (mXmppConnectionService.isDataSaverDisabled()) { - mXmppConnectionService.fetchAvatar(account, avatar); - } - } - if (mXmppConnectionService.isMuc(account, from)) { return; } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 08f6cdb2c69e65976dfc1ba384a986493959bb76..58e33094d99bd059f3acd2726af7f2530c5209fc 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -150,6 +150,7 @@ import im.conversations.android.xmpp.IqErrorException; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.up.Push; +import im.conversations.android.xmpp.model.vcard.update.VCardUpdate; import java.io.File; import java.security.Security; import java.security.cert.CertificateException; @@ -3958,18 +3959,25 @@ public class XmppConnectionService extends Service { final Conversation conversation, final OnConferenceConfigurationFetched callback) { final var account = conversation.getAccount(); final var connection = account.getXmppConnection(); + final var address = conversation.getJid().asBareJid(); if (connection == null) { return; } final var future = - connection - .getManager(DiscoManager.class) - .info(Entity.discoItem(conversation.getJid().asBareJid()), null); + connection.getManager(DiscoManager.class).info(Entity.discoItem(address), null); Futures.addCallback( future, new FutureCallback<>() { @Override public void onSuccess(InfoQuery result) { + final var avatarHash = + result.getServiceDiscoveryExtension( + Namespace.MUC_ROOM_INFO, "muc#roominfo_avatarhash"); + if (VCardUpdate.isValidSHA1(avatarHash)) { + connection + .getManager(AvatarManager.class) + .handleVCardUpdate(address, avatarHash); + } final MucOptions mucOptions = conversation.getMucOptions(); final Bookmark bookmark = conversation.getBookmark(); final boolean sameBefore = @@ -4403,177 +4411,6 @@ public class XmppConnectionService extends Service { } } - public void fetchAvatar(Account account, final Avatar avatar) { - final String KEY = generateFetchKey(account, avatar); - synchronized (this.mInProgressAvatarFetches) { - if (mInProgressAvatarFetches.add(KEY)) { - switch (avatar.origin) { - case PEP: - this.mInProgressAvatarFetches.add(KEY); - fetchAvatarPep(account, avatar, null); - break; - case VCARD: - this.mInProgressAvatarFetches.add(KEY); - fetchAvatarVcard(account, avatar); - break; - } - } else if (avatar.origin == Avatar.Origin.PEP) { - mOmittedPepAvatarFetches.add(KEY); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": already fetching " - + avatar.origin - + " avatar for " - + avatar.owner); - } - } - } - - private void fetchAvatarPep( - final Account account, final Avatar avatar, final UiCallback callback) { - final Iq packet = this.mIqGenerator.retrievePepAvatar(avatar); - sendIqPacket( - account, - packet, - (result) -> { - synchronized (mInProgressAvatarFetches) { - mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); - } - final String ERROR = - account.getJid().asBareJid() - + ": fetching avatar for " - + avatar.owner - + " failed "; - if (result.getType() == Iq.Type.RESULT) { - avatar.image = IqParser.avatarData(result); - if (avatar.image != null) { - if (getFileBackend().save(avatar)) { - if (account.getJid().asBareJid().equals(avatar.owner)) { - if (account.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(account); - } - getAvatarService().clear(account); - updateConversationUi(); - updateAccountUi(); - } else { - final Contact contact = - account.getRoster().getContact(avatar.owner); - contact.setAvatar(avatar.sha1sum); - account.getXmppConnection() - .getManager(RosterManager.class) - .writeToDatabaseAsync(); - getAvatarService().clear(contact); - updateConversationUi(); - updateRosterUi(); - } - if (callback != null) { - callback.success(avatar); - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": successfully fetched pep avatar for " - + avatar.owner); - return; - } - } else { - - Log.d(Config.LOGTAG, ERROR + "(parsing error)"); - } - } else { - Element error = result.findChild("error"); - if (error == null) { - Log.d(Config.LOGTAG, ERROR + "(server error)"); - } else { - Log.d(Config.LOGTAG, ERROR + error); - } - } - if (callback != null) { - callback.error(0, null); - } - }); - } - - private void fetchAvatarVcard(final Account account, final Avatar avatar) { - final var address = avatar.owner; - final var connection = account.getXmppConnection(); - final var future = connection.getManager(VCardManager.class).retrievePhoto(address); - Futures.addCallback( - future, - new FutureCallback<>() { - @Override - public void onSuccess(byte[] result) { - avatar.image = BaseEncoding.base64().encode(result); - if (fileBackend.save(avatar)) { - setVCardAvatar(account, avatar); - } - } - - @Override - public void onFailure(@NonNull Throwable t) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": could not retrieve vCard avatar of " - + avatar.owner); - } - }, - MoreExecutors.directExecutor()); - } - - // TODO move this into VCard manager - private void setVCardAvatar(final Account account, final Avatar avatar) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": successfully fetched vCard avatar for " - + avatar.owner); - if (avatar.owner.isBareJid()) { - if (account.getJid().asBareJid().equals(avatar.owner) && account.getAvatar() == null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": had no avatar. replacing with vcard"); - account.setAvatar(avatar.getFilename()); - databaseBackend.updateAccount(account); - getAvatarService().clear(account); - updateAccountUi(); - } else { - // TODO if this is a MUC clear MucOptions too - // TODO do the same clearing for when setting a cached version - final Contact contact = account.getRoster().getContact(avatar.owner); - contact.setAvatar(avatar.sha1sum); - account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync(); - getAvatarService().clear(contact); - updateRosterUi(); - } - updateConversationUi(); - } else { - Conversation conversation = find(account, avatar.owner.asBareJid()); - if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { - MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner); - if (user != null) { - if (user.setAvatar(avatar)) { - getAvatarService().clear(user); - updateConversationUi(); - updateMucRosterUi(); - } - // TODO don’t do that. this will put lower quality vCard avatars into contacts - if (user.getRealJid() != null) { - Contact contact = account.getRoster().getContact(user.getRealJid()); - contact.setAvatar(avatar.sha1sum); - account.getXmppConnection() - .getManager(RosterManager.class) - .writeToDatabaseAsync(); - getAvatarService().clear(contact); - updateRosterUi(); - } - } - } - } - } - public ListenableFuture checkForAvatar(final Account account) { final var connection = account.getXmppConnection(); return connection diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 37115cbae4c12793e80ea1f00a076d9de1ef19f2..a6131ac180d620a3cfb8470c48dd9c01157d9e47 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -47,6 +47,7 @@ public final class Namespace { public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB_EVENT = PUBSUB + "#event"; public static final String MUC = "http://jabber.org/protocol/muc"; + public static final String MUC_ROOM_INFO = MUC + "#roominfo"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; public static final String PUBSUB_CONFIG_NODE_MAX = PUBSUB + "#config-node-max"; public static final String PUBSUB_ERROR = PUBSUB + "#errors"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java index f60d45eb6f74907fa3c6a8184cc9299cc1218456..0c79c0708f42e300592ca555f498d213c3937c9f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -24,6 +24,8 @@ import com.google.common.util.concurrent.SettableFuture; import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.Compatibility; @@ -38,6 +40,7 @@ import im.conversations.android.xmpp.model.avatar.Info; import im.conversations.android.xmpp.model.avatar.Metadata; import im.conversations.android.xmpp.model.pubsub.Items; import im.conversations.android.xmpp.model.upload.purpose.Profile; +import im.conversations.android.xmpp.model.vcard.update.VCardUpdate; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -231,11 +234,23 @@ public class AvatarManager extends AbstractManager { throw new IOException("Could not move avatar to avatar location"); } - private void setAvatar(final Jid from, final Info info) { - Log.d(Config.LOGTAG, "setting avatar for " + from + " to " + info.getId()); + private void setAvatar(final Jid address, final Info info) { + setAvatar(address, info.getId()); + } + + private void setAvatar(final Jid from, final String id) { + Log.d(Config.LOGTAG, "setting avatar for " + from + " to " + id); + if (from.isBareJid()) { + setAvatarContact(from, id); + } else { + setAvatarMucUser(from, id); + } + } + + private void setAvatarContact(final Jid from, final String id) { final var account = getAccount(); if (account.getJid().asBareJid().equals(from)) { - if (account.setAvatar(info.getId())) { + if (account.setAvatar(id)) { getDatabase().updateAccount(account); service.notifyAccountAvatarHasChanged(account); } @@ -244,15 +259,38 @@ public class AvatarManager extends AbstractManager { service.updateAccountUi(); } else { final Contact contact = account.getRoster().getContact(from); - if (contact.setAvatar(info.getId())) { + if (contact.setAvatar(id)) { connection.getManager(RosterManager.class).writeToDatabaseAsync(); service.getAvatarService().clear(contact); + + final var conversation = service.find(account, from); + if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) { + service.getAvatarService().clear(conversation.getMucOptions()); + } + service.updateConversationUi(); service.updateRosterUi(); } } } + private void setAvatarMucUser(final Jid from, final String id) { + final var account = getAccount(); + final Conversation conversation = service.find(account, from.asBareJid()); + if (conversation == null || conversation.getMode() != Conversation.MODE_MULTI) { + return; + } + final var user = conversation.getMucOptions().findUserByFullJid(from); + if (user == null) { + return; + } + if (user.setAvatar(id)) { + service.getAvatarService().clear(user); + service.updateConversationUi(); + service.updateMucRosterUi(); + } + } + public void handleItems(final Jid from, final Items items) { final var account = getAccount(); // TODO support retract @@ -296,6 +334,38 @@ public class AvatarManager extends AbstractManager { } } + public void handleVCardUpdate(final Jid address, final VCardUpdate vCardUpdate) { + final var hash = vCardUpdate.getHash(); + if (hash == null) { + return; + } + handleVCardUpdate(address, hash); + } + + public void handleVCardUpdate(final Jid address, final String hash) { + Preconditions.checkArgument(VCardUpdate.isValidSHA1(hash)); + final var avatarFile = FileBackend.getAvatarFile(context, hash); + if (avatarFile.exists()) { + setAvatar(address, hash); + } else if (service.isDataSaverDisabled()) { + final var future = this.fetchAndStoreVCard(address, hash); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Void result) { + Log.d(Config.LOGTAG, "successfully fetch vCard avatar for " + address); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "could not fetch avatar for " + address, t); + } + }, + MoreExecutors.directExecutor()); + } + } + private PreferredFallback getPreferredFallback(final Map.Entry entry) { final var mainItemId = entry.getKey(); final var infos = entry.getValue().getInfos(); @@ -617,6 +687,36 @@ public class AvatarManager extends AbstractManager { } } + public ListenableFuture fetchAndStoreVCard(final Jid address, final String expectedHash) { + final var future = connection.getManager(VCardManager.class).retrievePhoto(address); + return Futures.transformAsync( + future, + photo -> { + final var actualHash = Hashing.sha1().hashBytes(photo).toString(); + if (!actualHash.equals(expectedHash)) { + return Futures.immediateFailedFuture( + new IllegalStateException( + String.format( + "Hash in vCard update for %s did not match", + address))); + } + final var avatarFile = FileBackend.getAvatarFile(context, actualHash); + if (avatarFile.exists()) { + setAvatar(address, actualHash); + return Futures.immediateVoidFuture(); + } + final var writeFuture = write(avatarFile, photo); + return Futures.transform( + writeFuture, + v -> { + setAvatar(address, actualHash); + return null; + }, + MoreExecutors.directExecutor()); + }, + AVATAR_COMPRESSION_EXECUTOR); + } + public enum ImageFormat { PNG, JPEG, diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java index 068e59b89d9d6c808d87740dd228e4519d6915db..e8782f9a5429c3c5f2ae7e6d50bdc73d4422e6d0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -3,9 +3,7 @@ package eu.siacs.conversations.xmpp.pep; import android.util.Base64; import androidx.annotation.NonNull; import com.google.common.base.MoreObjects; -import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; -import im.conversations.android.xmpp.model.avatar.Metadata; import okhttp3.HttpUrl; public class Avatar { @@ -49,42 +47,6 @@ public class Avatar { return sha1sum; } - public static Avatar parseMetadata(final String primaryId, final Metadata metadata) { - if (primaryId == null || metadata == null) { - return null; - } - for (Element child : metadata.getChildren()) { - if (child.getName().equals("info") && primaryId.equals(child.getAttribute("id"))) { - Avatar avatar = new Avatar(); - String height = child.getAttribute("height"); - String width = child.getAttribute("width"); - String size = child.getAttribute("bytes"); - try { - if (height != null) { - avatar.height = Integer.parseInt(height); - } - if (width != null) { - avatar.width = Integer.parseInt(width); - } - if (size != null) { - avatar.size = Long.parseLong(size); - } - } catch (NumberFormatException e) { - return null; - } - avatar.type = child.getAttribute("type"); - String hash = child.getAttribute("id"); - if (!isValidSHA1(hash)) { - return null; - } - avatar.sha1sum = hash; - avatar.origin = Origin.PEP; - return avatar; - } - } - return null; - } - @Override public boolean equals(Object object) { if (object != null && object instanceof Avatar other) { @@ -93,22 +55,4 @@ public class Avatar { return false; } } - - public static Avatar parsePresence(Element x) { - String hash = x == null ? null : x.findChildContent("photo"); - if (hash == null) { - return null; - } - if (!isValidSHA1(hash)) { - return null; - } - Avatar avatar = new Avatar(); - avatar.sha1sum = hash; - avatar.origin = Origin.VCARD; - return avatar; - } - - private static boolean isValidSHA1(String s) { - return s != null && s.matches("[a-fA-F0-9]{40}"); - } } diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java b/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java index 0be3f94b9fef7428b4977ebde3cdb728f55b1e7e..231d1074d1450c4ac4baaa8f8356d7d32f164f3b 100644 --- a/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java @@ -16,6 +16,11 @@ public class VCardUpdate extends Extension { public String getHash() { final var photo = getPhoto(); - return photo == null ? null : photo.getContent(); + final var hash = photo == null ? null : photo.getContent(); + return isValidSHA1(hash) ? hash : null; + } + + public static boolean isValidSHA1(final String s) { + return s != null && s.matches("[a-fA-F0-9]{40}"); } }