Blogs

Using Gravatars...

So I'm here late one night and I'm wondering why Liferay is hosting profile pics...

I mean, in this day and age we all have accounts all over the place and many of those places also support profile pics/avatars.  In each of these sites when we create accounts we go and pull our standard avatar pic in and move on.

Recently, however, I was working with a site that used an avatar web service and I thought "wow, this is cool.".

Basically the fine folks at http://www.gravatar.com have created this site where they host avatar images for you.  As a user you create an account (keyed off your email address) and then load your avatar pic.

Then, on any site that points to gravatar and knows your email address, they can display your gravatar image.  You don't have to have a copy on the separate sites anymore!

And from a user perspective, you get the benefit of being able to change your avatar on one site and all of the other gravatar-aware sites will update automatically.

So my first thought was how I could bring this to Liferay...

Liferay Avatars

So the Liferay profile pics/avatars are all handled by the getPortraitURL() method of the User model class.  At first blush this would seem like an easy way to override Liferay's default portrait URL handling.

All we need to do is create a class which extends UserWrapper and overrides the getPortraitURL() method.  I'm including the code below that will actually do the job:

/**
 * GRAVATAR_BASE_URL: The base URL to use for non-secure gravatars.
 */
public static final String GRAVATAR_BASE_URL = "http://www.gravatar.com/avatar/";
/**
 * GRAVATAR_SECURE_BASE_URL: The base URL to use for secure gravatars.
 */
public static final String GRAVATAR_SECURE_BASE_URL = "https://secure.gravatar.com/avatar/";

/**
 * getPortraitURL: Overriding method to return the gravatar URL instead of the portal's URL.
 * @param themeDisplay
 * @return String The gravatar URL.
 * @throws PortalException
 * @throws SystemException
 */
@Override
public String getPortraitURL(ThemeDisplay themeDisplay) throws PortalException, SystemException {
	String emailAddress = getEmailAddress();

	String hash = "00000000000000000000000000000000";

	if ((emailAddress == null) || (emailAddress.trim().length() < 1)) {
		// no email address

	} else {
		hash = md5Hex(emailAddress.trim().toLowerCase());
	}

	String def = super.getPortraitURL(themeDisplay);

	StringBuilder sb = new StringBuilder();

	boolean secure = StringUtil.equalsIgnoreCase(Http.HTTPS, PropsUtil.get(PropsKeys.WEB_SERVER_PROTOCOL));

	if (secure) {
		sb.append(GRAVATAR_SECURE_BASE_URL);
	} else {
		sb.append(GRAVATAR_BASE_URL);
	}

	// add the hash value
	sb.append(hash);

	if ((def != null) && (def.trim().length() > 0)) {
		// add the default param
		try {
			String url = URLEncoder.encode(def.trim(), "UTF-8");
			sb.append("?d=").append(url);
		} catch (UnsupportedEncodingException e) {
		}
	}

	return sb.toString();
}

/**
 * hex: Utility function from gravatar to hex a byte array.
 * @param array
 * @return String The hex string.
 * @link https://en.gravatar.com/site/implement/images/java/
 */
public static String hex(byte[] array) {
	StringBuffer sb = new StringBuffer();

	for (int i = 0; i < array.length; ++i) {
		sb.append(Integer.toHexString((array[i]	& 0xFF) | 0x100).substring(1,3));
	}

	return sb.toString();
}

/**
 * md5Hex: Utility function to create an MD5 hex string from a message.
 * @param message
 * @return String The md5 hex.
 * @link https://en.gravatar.com/site/implement/images/java/
 */
public static String md5Hex (String message) {
	try {
		MessageDigest md = MessageDigest.getInstance("MD5");
		return hex (md.digest(message.getBytes("CP1252")));
	} catch (NoSuchAlgorithmException e) {
	} catch (UnsupportedEncodingException e) {
	}

	return null;
}

The great part about this implementation is that if the user does not have a gravatar account, the Liferay profile pic will end up being used instead.

Liferay Service Wrappers

So now you have a UserWrapper extension class that uses gravatar.com, but how do you get Liferay to use it?

Well you create one or more service wrappers and override the necessary methods so your UserWrapper will be returned.

Sounds easy, right?  Well it isn't.

The first obvious wrapper to create is the UserLocalServiceWrapper extension class.  That's where the User records are returned, so that's a good place to start.

First we'll add some support methods to handle wrapping the users:

/**
 * wrap: Handles the wrapping of a single instance.
 * @param user
 * @return User The wrapped user.
 */
protected User wrap(final User user) {
	if (user == null) return null;

	// if the user is already wrapped, no reason to wrap again, just return it.
	if (ClassUtils.isAssignable(user.getClass(), GravatarUserWrapper.class)) {
		return user;
	}

	return new GravatarUserWrapper(user);
}

/**
 * wrap: Handles the wrapping of multiple instances in a list.
 * @param users
 * @return List The list of wrapped users.
 */
protected List<User> wrap(final List<User> users) {
	if ((users == null) || (users.isEmpty())) return users;

	// create a list to put the wrapped users in to return.
	List<User> toReturn = new ArrayList<User>(users.size());

	for (User user : users) {
		toReturn.add(wrap(user));
	}

	return toReturn;
}

That's actually the easy part.  The next part is to attempt override of each method that returns a User instance or a List of users:

 

@Override
public User addDefaultAdminUser(long companyId, String screenName, String emailAddress, Locale locale, String firstName, String middleName, String lastName) throws PortalException, SystemException {
	return wrap(super.addDefaultAdminUser(companyId, screenName, emailAddress, locale, firstName, middleName, lastName));
}

@Override
public User addUser(long creatorUserId, long companyId, boolean autoPassword, String password1, String password2, boolean autoScreenName, String screenName, String emailAddress, long facebookId, String openId, Locale locale, String firstName, String middleName, String lastName, int prefixId, int suffixId, boolean male, int birthdayMonth, int birthdayDay, int birthdayYear, String jobTitle, long[] groupIds, long[] organizationIds, long[] roleIds, long[] userGroupIds, boolean sendEmail, ServiceContext serviceContext) throws PortalException, SystemException {
	return wrap(super.addUser(creatorUserId, companyId, autoPassword, password1, password2, autoScreenName, screenName, emailAddress, facebookId, openId, locale, firstName, middleName, lastName, prefixId, suffixId, male, birthdayMonth, birthdayDay, birthdayYear, jobTitle, groupIds, organizationIds, roleIds, userGroupIds, sendEmail, serviceContext));
}

@Override
public User addUser(User user) throws SystemException {
	return wrap(super.addUser(user));
}

@Override
public User addUserWithWorkflow(long creatorUserId, long companyId, boolean autoPassword, String password1, String password2, boolean autoScreenName, String screenName, String emailAddress, long facebookId, String openId, Locale locale, String firstName, String middleName, String lastName, int prefixId, int suffixId, boolean male, int birthdayMonth, int birthdayDay, int birthdayYear, String jobTitle, long[] groupIds, long[] organizationIds, long[] roleIds, long[] userGroupIds, boolean sendEmail, ServiceContext serviceContext) throws PortalException, SystemException {
	return wrap(super.addUserWithWorkflow(creatorUserId, companyId, autoPassword, password1, password2, autoScreenName, screenName, emailAddress, facebookId, openId, locale, firstName, middleName, lastName, prefixId, suffixId, male, birthdayMonth, birthdayDay, birthdayYear, jobTitle, groupIds, organizationIds, roleIds, userGroupIds, sendEmail, serviceContext));
}

@Override
public User fetchUser(long userId) throws SystemException {
	return wrap(super.fetchUser(userId));
}

@Override
public User fetchUserByEmailAddress(long companyId, String emailAddress) throws SystemException {
	return wrap(super.fetchUserByEmailAddress(companyId, emailAddress));
}

@Override
public User fetchUserByFacebookId(long companyId, long facebookId) throws SystemException {
	return wrap(super.fetchUserByFacebookId(companyId, facebookId));
}

@Override
public User fetchUserById(long userId) throws SystemException {
	return wrap(super.fetchUserById(userId));
}

@Override
public User fetchUserByOpenId(long companyId, String openId) throws SystemException {
	return wrap(super.fetchUserByOpenId(companyId, openId));
}

@Override
public User fetchUserByScreenName(long companyId, String screenName) throws SystemException {
	return wrap(super.fetchUserByScreenName(companyId, screenName));
}

@Override
public User fetchUserByUuidAndCompanyId(String uuid, long companyId) throws SystemException {
	return wrap(super.fetchUserByUuidAndCompanyId(uuid, companyId));
}

@Override
public List<User> getCompanyUsers(long companyId, int start, int end) throws SystemException {
	return wrap(super.getCompanyUsers(companyId, start, end));
}

@Override
public User getDefaultUser(long companyId) throws PortalException, SystemException {
	return wrap(super.getDefaultUser(companyId));
}

@Override
public List<User> getGroupUsers(long groupId) throws SystemException {
	return wrap(super.getGroupUsers(groupId));
}

@Override
public List<User> getGroupUsers(long groupId, int start, int end) throws SystemException {
	return wrap(super.getGroupUsers(groupId, start, end));
}

@Override
public List<User> getGroupUsers(long groupId, int start, int end, OrderByComparator orderByComparator) throws SystemException {
	return wrap(super.getGroupUsers(groupId, start, end, orderByComparator));
}

@Override
public List<User> getInheritedRoleUsers(long roleId, int start, int end, OrderByComparator obc) throws PortalException, SystemException {
	return wrap(super.getInheritedRoleUsers(roleId, start, end, obc));
}

@Override
public List<User> getNoAnnouncementsDeliveries(String type) throws SystemException {
	return wrap(super.getNoAnnouncementsDeliveries(type));
}

@Override
public List<User> getNoContacts() throws SystemException {
	return wrap(super.getNoContacts());
}

@Override
public List<User> getNoGroups() throws SystemException {
	return wrap(super.getNoGroups());
}

@Override
public List<User> getOrganizationUsers(long organizationId) throws SystemException {
	return wrap(super.getOrganizationUsers(organizationId));
}

@Override
public List<User> getOrganizationUsers(long organizationId, int start, int end) throws SystemException {
	return wrap(super.getOrganizationUsers(organizationId, start, end));
}

@Override
public List<User> getOrganizationUsers(long organizationId, int start, int end, OrderByComparator orderByComparator) throws SystemException {
	return wrap(super.getOrganizationUsers(organizationId, start, end, orderByComparator));
}

@Override
public List<User> getRoleUsers(long roleId) throws SystemException {
	return wrap(super.getRoleUsers(roleId));
}

@Override
public List<User> getRoleUsers(long roleId, int start, int end) throws SystemException {
	return wrap(super.getRoleUsers(roleId, start, end));
}

@Override
public List<User> getRoleUsers(long roleId, int start, int end, OrderByComparator orderByComparator) throws SystemException {
	return wrap(super.getRoleUsers(roleId, start, end, orderByComparator));
}

@Override
public List<User> getSocialUsers(long userId1, long userId2, int start, int end, OrderByComparator obc) throws PortalException, SystemException {
	return wrap(super.getSocialUsers(userId1, userId2, start, end, obc));
}

@Override
public List<User> getSocialUsers(long userId1, long userId2, int type, int start, int end, OrderByComparator obc) throws PortalException, SystemException {
	return wrap(super.getSocialUsers(userId1, userId2, type, start, end, obc));
}

@Override
public List<User> getSocialUsers(long userId, int start, int end, OrderByComparator obc) throws PortalException, SystemException {
	return wrap(super.getSocialUsers(userId, start, end, obc));
}

@Override
public List<User> getSocialUsers(long userId, int type, int start, int end, OrderByComparator obc) throws PortalException, SystemException {
	return wrap(super.getSocialUsers(userId, type, start, end, obc));
}

@Override
public List<User> getTeamUsers(long teamId) throws SystemException {
	return wrap(super.getTeamUsers(teamId));
}

@Override
public List<User> getTeamUsers(long teamId, int start, int end) throws SystemException {
	return wrap(super.getTeamUsers(teamId, start, end));
}

@Override
public List<User> getTeamUsers(long teamId, int start, int end, OrderByComparator orderByComparator) throws SystemException {
	return wrap(super.getTeamUsers(teamId, start, end, orderByComparator));
}

@Override
public User getUser(long userId) throws PortalException, SystemException {
	return wrap(super.getUser(userId));
}

@Override
public User getUserByContactId(long contactId) throws PortalException, SystemException {
	return wrap(super.getUserByContactId(contactId));
}

@Override
public User getUserByEmailAddress(long companyId, String emailAddress) throws PortalException, SystemException {
	return wrap(super.getUserByEmailAddress(companyId, emailAddress));
}

@Override
public User getUserByFacebookId(long companyId, long facebookId) throws PortalException, SystemException {
	return wrap(super.getUserByFacebookId(companyId, facebookId));
}

@Override
public User getUserById(long companyId, long userId) throws PortalException, SystemException {
	return wrap(super.getUserById(companyId, userId));
}

@Override
public User getUserById(long userId) throws PortalException, SystemException {
	return wrap(super.getUserById(userId));
}

@Override
public User getUserByOpenId(long companyId, String openId) throws PortalException, SystemException {
	return wrap(super.getUserByOpenId(companyId, openId));
}

@Override
public User getUserByPortraitId(long portraitId) throws PortalException, SystemException {
	return wrap(super.getUserByPortraitId(portraitId));
}

@Override
public User getUserByScreenName(long companyId, String screenName) throws PortalException, SystemException {
	return wrap(super.getUserByScreenName(companyId, screenName));
}

@Override
public User getUserByUuidAndCompanyId(String uuid, long companyId) throws PortalException, SystemException {
	return wrap(super.getUserByUuidAndCompanyId(uuid, companyId));
}

@Override
public List<User> getUserGroupUsers(long userGroupId) throws SystemException {
	return wrap(super.getUserGroupUsers(userGroupId));
}

@Override
public List<User> getUserGroupUsers(long userGroupId, int start, int end) throws SystemException {
	return wrap(super.getUserGroupUsers(userGroupId, start, end));
}

@Override
public List<User> getUserGroupUsers(long userGroupId, int start, int end, OrderByComparator orderByComparator) throws SystemException {
	return wrap(super.getUserGroupUsers(userGroupId, start, end, orderByComparator));
}

@Override
public List<User> getUsers(int start, int end) throws SystemException {
	return wrap(super.getUsers(start, end));
}

@Override
public List<User> search(long companyId, String firstName, String middleName, String lastName, String screenName, String emailAddress, int status, LinkedHashMap<String, Object> params, boolean andSearch, int start, int end, OrderByComparator obc) throws SystemException {
	return wrap(super.search(companyId, firstName, middleName, lastName, screenName, emailAddress, status, params, andSearch, start, end, obc));
}

@Override
public List<User> search(long companyId, String keywords, int status, LinkedHashMap<String, Object> params, int start, int end, OrderByComparator obc) throws SystemException {
	return wrap(super.search(companyId, keywords, status, params, start, end, obc));
}

@Override
public User updateAgreedToTermsOfUse(long userId, boolean agreedToTermsOfUse) throws PortalException, SystemException {
	return wrap(super.updateAgreedToTermsOfUse(userId, agreedToTermsOfUse));
}

@Override
public User updateEmailAddress(long userId, String password, String emailAddress1, String emailAddress2) throws PortalException, SystemException {
	return wrap(super.updateEmailAddress(userId, password, emailAddress1, emailAddress2));
}

@Override
public User updateEmailAddress(long userId, String password, String emailAddress1, String emailAddress2, ServiceContext serviceContext) throws PortalException, SystemException {
	return wrap(super.updateEmailAddress(userId, password, emailAddress1, emailAddress2, serviceContext));
}

@Override
public User updateEmailAddressVerified(long userId, boolean emailAddressVerified) throws PortalException, SystemException {
	return wrap(super.updateEmailAddressVerified(userId, emailAddressVerified));
}

@Override
public User updateFacebookId(long userId, long facebookId) throws PortalException, SystemException {
	return wrap(super.updateFacebookId(userId, facebookId));
}

@Override
public User updateIncompleteUser(long creatorUserId, long companyId, boolean autoPassword, String password1, String password2, boolean autoScreenName, String screenName, String emailAddress, long facebookId, String openId, Locale locale, String firstName, String middleName, String lastName, int prefixId, int suffixId, boolean male, int birthdayMonth, int birthdayDay, int birthdayYear, String jobTitle, boolean updateUserInformation, boolean sendEmail, ServiceContext serviceContext) throws PortalException, SystemException {
	return wrap(super.updateIncompleteUser(creatorUserId, companyId, autoPassword, password1, password2, autoScreenName, screenName, emailAddress, facebookId, openId, locale, firstName, middleName, lastName, prefixId, suffixId, male, birthdayMonth, birthdayDay, birthdayYear, jobTitle, updateUserInformation, sendEmail, serviceContext));
}

@Override
public User updateJobTitle(long userId, String jobTitle) throws PortalException, SystemException {
	return wrap(super.updateJobTitle(userId, jobTitle));
}

@Override
public User updateLastLogin(long userId, String loginIP) throws PortalException, SystemException {
	return wrap(super.updateLastLogin(userId, loginIP));
}

@Override
public User updateLockout(User user, boolean lockout) throws PortalException, SystemException {
	return wrap(super.updateLockout(user, lockout));
}

@Override
public User updateLockoutByEmailAddress(long companyId, String emailAddress, boolean lockout) throws PortalException, SystemException {
	return wrap(super.updateLockoutByEmailAddress(companyId, emailAddress, lockout));
}

@Override
public User updateLockoutById(long userId, boolean lockout) throws PortalException, SystemException {
	return wrap(super.updateLockoutById(userId, lockout));
}

@Override
public User updateLockoutByScreenName(long companyId, String screenName, boolean lockout) throws PortalException, SystemException {
	return wrap(super.updateLockoutByScreenName(companyId, screenName, lockout));
}

@Override
public User updateModifiedDate(long userId, Date modifiedDate) throws PortalException, SystemException {
	return wrap(super.updateModifiedDate(userId, modifiedDate));
}

@Override
public User updateOpenId(long userId, String openId) throws PortalException, SystemException {
	return wrap(super.updateOpenId(userId, openId));
}

@Override
public User updatePassword(long userId, String password1, String password2, boolean passwordReset) throws PortalException, SystemException {
	return wrap(super.updatePassword(userId, password1, password2, passwordReset));
}

@Override
public User updatePassword(long userId, String password1, String password2, boolean passwordReset, boolean silentUpdate) throws PortalException, SystemException {
	return wrap(super.updatePassword(userId, password1, password2, passwordReset, silentUpdate));
}

@Override
public User updatePasswordManually(long userId, String password, boolean passwordEncrypted, boolean passwordReset, Date passwordModifiedDate) throws PortalException, SystemException {
	return wrap(super.updatePasswordManually(userId, password, passwordEncrypted, passwordReset, passwordModifiedDate));
}

@Override
public User updatePasswordReset(long userId, boolean passwordReset) throws PortalException, SystemException {
	return wrap(super.updatePasswordReset(userId, passwordReset));
}

@Override
public User updatePortrait(long userId, byte[] bytes) throws PortalException, SystemException {
	return wrap(super.updatePortrait(userId, bytes));
}

@Override
public User updateReminderQuery(long userId, String question, String answer) throws PortalException, SystemException {
	return wrap(super.updateReminderQuery(userId, question, answer));
}

@Override
public User updateScreenName(long userId, String screenName) throws PortalException, SystemException {
	return wrap(super.updateScreenName(userId, screenName));
}

@Override
public User updateStatus(long userId, int status, ServiceContext serviceContext) throws PortalException, SystemException {
	return wrap(super.updateStatus(userId, status, serviceContext));
}

@Override
public User updateUser(User user) throws SystemException {
	return wrap(super.updateUser(user));
}

@Override
public User updateUser(long userId, String oldPassword, String newPassword1, String newPassword2, boolean passwordReset, String reminderQueryQuestion, String reminderQueryAnswer, String screenName, String emailAddress, long facebookId, String openId, String languageId, String timeZoneId, String greeting, String comments, String firstName, String middleName, String lastName, int prefixId, int suffixId, boolean male, int birthdayMonth, int birthdayDay, int birthdayYear, String smsSn, String aimSn, String facebookSn, String icqSn, String jabberSn, String msnSn, String mySpaceSn, String skypeSn, String twitterSn, String ymSn, String jobTitle, long[] groupIds, long[] organizationIds, long[] roleIds, List<UserGroupRole> userGroupRoles, long[] userGroupIds, ServiceContext serviceContext) throws PortalException, SystemException {
	return wrap(super.updateUser(userId, oldPassword, newPassword1, newPassword2, passwordReset, reminderQueryQuestion, reminderQueryAnswer, screenName, emailAddress, facebookId, openId, languageId, timeZoneId, greeting, comments, firstName, middleName, lastName, prefixId, suffixId, male, birthdayMonth, birthdayDay, birthdayYear, smsSn, aimSn, facebookSn, icqSn, jabberSn, msnSn, mySpaceSn, skypeSn, twitterSn, ymSn, jobTitle, groupIds, organizationIds, roleIds, userGroupRoles, userGroupIds, serviceContext));
}

Okay, although this seems like a lot of methods, at least we're done now, right?

Eh, not so fast...

There are still some holes in our implementation.  For example, any DynamicQuery that is returning users would not be wrapped.  Any custom queries elsewhere that are returning users would not be wrapped.  Any other service that is returning a User or Users that don't go through the UserLocalService, well those would not be wrapped either.

Conclusion

But actually I think we're in pretty good shape.  We're hitting most of the major points where User objects are being returned, all of those users will be appropriately wrapped to return the gravatar URLs for the portrait pics...

A final suggestion I would leave with you - if you're using gravatar.com you may not want to support changing profile pics anymore.  All you should need to do is add to your portal-ext.properties file a line like:

field.editable.domains[portrait]=

Any user that has an email address that matches the domain specified in this line will be able to update the portrait.  When it's empty like above, users should not be able to edit the portrait.

Should, but not definite, because there's other field.editable properties which have higher priority than the specific field check, like field.editable.roles=administrator allows admins to edit all fields including the portrait image.

But that is just a matter of training your admins not to edit the portraits and you should be fine.

Anyway you'll want to build this as a service hook plugin and deploy into your environment and your users should start seeing their gravatar profile pics.

 

Nice post.

A cleaner solution might be to write a servlet filter or a ServicePreAction hook that listens for the default portrait URL (/image/user_male_portrait?img_id=23360&img_id_token=...&t=...), use the img_id parameter to find out which user it belongs to and use his/her emailadress for the translation to a Gravatar url? I think this kind of solution would be less intrusive.
If you can return the right image link to the browser, then the browser will issue the request.

If you're intercepting as a servlet filter then you have to proxy the call to the external service and feed the binary data back.

Some enterprises do not like having their DMZ establish outbound HTTP connections and question that kind of thing as a security vulnerability.

There's also a question of scalability and whether you really want your server handling these things at all if they can be avoided.

If these issues are not concerns, you're absolutely right that the servlet filter or ServicePreAction would be a less-intrusive path.

The wrapper definitely has holes, so combining it w/ the servlet filter or ServicePreAction could be a simpler way to patch all of the holes. Rather than dealing with all DQ and custom query invocation, you fall back to allowing the servlet filter to be the "catch all". Since it is the path less often taken scalability is not an issue and, as a one-off, it may be easier justifying proxying the external web call...