As you may know Exchange 2010 and Outlook 2010 use the Picture attribute (better known as thumbnailPhoto based on its LDAP name) to store user and contact photos in Active Directory. You can learn more about that from this post in the Microsoft Exchange Team Blog.
Since I found no way in Outlook 2010 to set the photo, and found only (for an end user) cumbersome admin tools, like PowerShell and VBScript to upload the image. On the other side, SharePoint makes it possible for a user to upload her / his photo to the profile from the web UI easily.
Before trying to reinvent the wheel, I looked around on the web for similar solutions, but found only implementations (like this one) where an extension attribute of AD (like extensionAttribute3) was mapped to the ProfileURL SharePoint profile property.
My goal was a bit more: to synchronize the image binary as well, not only URLs. As I’ve already created tools to upload user photos from file system to SharePoint profile programmatically I thought it would be nice to reuse that knowledge on this task.
Although an ideal solution might be some kind of server process (for example a timer job), for the sake of simplicity and visibility I chose a Visual Web Part-based “self-service” implementation.
Let’s see the results first. You can find the code and the binaries as a single file download here.
When deploy the solution, you have to add the Profile Photo Sync WebPart to a page…
…and set its Root application partition property to the root of your AD.
Assuming there is no user photo in the profile nor in the AD, you should see something similar first:
Let’s upload our photo quickly to our SharePoint profile. It is done, the image is shown on the profile page. Let’s back to the web part. But wait a minute! The photo is still not visible there! What happened? I told you: wait a minute. SharePoint requires about one minute to push the profile property changes to the site. After it is done, the image is visible in the web part as expected:
If you press the >> button, the image is transferred to AD.
Now delete the image from the profile and refresh the page. The image can be found in the AD, but not in the SharePoint profile.
Try to load it back to SharePoint by pressing the << button. It seems to have no effect, but again you should wait a minute to see the result:
If you delete the image from the SharePoint profile again, refresh the page and now click on the >> button, you find that the photo is deleted from the AD as well and we are back to our starting point. Synchronizing an empty thumbnailPhoto property from AD to SharePoint profile deletes the image from the profile as well.
After this short demo let’s see some code blocks.
Since our control has to know where to look for AD properties we have to provide this iannformation for it. The web part has a property called DefaultPartition. The value of this property is forwarded to the user control in the CreateChildControls method.
- [Personalizable(),
- WebBrowsable(),
- WebDisplayName("Root application partition")]
- public string DefaultPartition
- {
- get;
- set;
- }
- #endregion
- protected override void CreateChildControls()
- {
- ProfilePhotoSyncWebPartUserControl control = (ProfilePhotoSyncWebPartUserControl)Page.LoadControl(_ascxPath);
- control.DefaultPartition = DefaultPartition;
- Controls.Add(control);
- }
The RefreshControls method is responsible for rendering the content of the UI. Since there is no built-in way to refer the thumbnailPhoto property in the AD via a URL, I made a quick and dirty hack there. If the request URL contains the UserName query string parameter, we clears the response, get the binary content from the thumbnailPhoto property and push that content to the response.
If the UserName query string parameter is not found in the request URL and the thumbnailPhoto property is not empty, I set the image URL of the AD image to the URL of the current page appending the current user name in the UserName query string parameter.
Similarly, if the PictureUrl user profile property is not empty in SharePoint I set the image URL for the SharePoint image to the value stored in the PictureUrl property.
If one or both of the properties would be empty then the corresponding image is hidden and a No photo label is shown as you can see on the images above.
- private void RefreshControls()
- {
- String userName = Request.QueryString["UserName"];
- // hack: render response as image for AD "thumbnailPhoto" image
- if (!String.IsNullOrEmpty(userName))
- {
- Response.Clear();
- ADUtils adUtils = new ADUtils(DefaultPartition);
- using (DirectoryEntry root = adUtils.GetDefaultPartition())
- using (DirectoryEntry user = adUtils.FindUserByAccountName(userName))
- {
- byte[] thumbnailPhotoBytes = (byte[])user.Properties["thumbnailPhoto"].Value;
- if (thumbnailPhotoBytes != null)
- {
- Response.BinaryWrite(thumbnailPhotoBytes);
- }
- }
- Response.End();
- }
- else
- {
- try
- {
- ADUtils adUtils = new ADUtils(DefaultPartition);
- using (DirectoryEntry root = adUtils.GetDefaultPartition())
- using (DirectoryEntry user = adUtils.FindUserByAccountName(_shortName))
- {
- byte[] thumbnailPhotoBytes = (byte[])user.Properties["thumbnailPhoto"].Value;
- bool hasThumbnailPhoto = (thumbnailPhotoBytes != null);
- ADImage.Visible = hasThumbnailPhoto;
- ADImageLabel.Visible = !hasThumbnailPhoto;
- if (hasThumbnailPhoto)
- {
- ADImage.ImageUrl = String.Format("{0}?UserName={1}", Request.Url.GetLeftPart(UriPartial.Path), _shortName);
- }
- }
- }
- catch (Exception ex)
- {
- Warning.Visible = true;
- }
- UserProfileManager userProfileManager = new UserProfileManager(SPServiceContext.Current);
- UserProfile userProfile = userProfileManager.GetUserProfile(_accountName);
- String pictureUrl = (String)userProfile["PictureUrl"].Value;
- bool hasProfilelPhoto = (!String.IsNullOrEmpty(pictureUrl));
- // double check to be sure the file is there to avoid "missing" images on the page
- // when removing the profile image on the UI it takes some time the change get reflected
- // in the profile property, but image is deleted immediately
- if (hasProfilelPhoto)
- {
- using (SPSite site = new SPSite(pictureUrl))
- {
- using (SPWeb web = site.OpenWeb())
- {
- SPFile file = web.GetFile(pictureUrl);
- hasProfilelPhoto = file.Exists;
- }
- }
- }
- SPImage.Visible = hasProfilelPhoto;
- SPImageLabel.Visible = !hasProfilelPhoto;
- if (hasProfilelPhoto != null)
- {
- SPImage.ImageUrl = pictureUrl;
- }
- }
- }
When you click on the >> button to copy SharePoint profile image to AD the following code is called:
- protected void SP2AD_Click(object sender, EventArgs e)
- {
- UserProfileManager userProfileManager = new UserProfileManager(SPServiceContext.Current);
- UserProfile userProfile = userProfileManager.GetUserProfile(_accountName);
- String pictureUrl = (String)userProfile["PictureUrl"].Value;
- bool hasProfilelPhoto = (!String.IsNullOrEmpty(pictureUrl));
- byte[] imageContent = null;
- // double check to be sure the file is there to avoid "missing" images on the page
- // when removing the profile image on the UI it takes some time the change get reflected
- // in the profile property, but image is deleted immediately
- if (hasProfilelPhoto)
- {
- using (SPSite site = new SPSite(pictureUrl))
- {
- using (SPWeb web = site.OpenWeb())
- {
- SPFile file = web.GetFile(pictureUrl);
- if (file.Exists)
- {
- imageContent = file.OpenBinary();
- }
- }
- }
- }
- ADUtils adUtils = new ADUtils(DefaultPartition);
- using (DirectoryEntry root = adUtils.GetDefaultPartition())
- using (DirectoryEntry user = adUtils.FindUserByAccountName(_shortName))
- {
- user.Properties["thumbnailPhoto"].Clear();
- if (imageContent != null)
- {
- user.Properties["thumbnailPhoto"].Add(imageContent);
- }
- user.CommitChanges();
- }
- RefreshControls();
- }
Similarly, when you click << to copy image from AD to the SharePoint profile, the following code gets executed:
- protected void AD2SP_Click(object sender, EventArgs e)
- {
- try
- {
- ADUtils adUtils = new ADUtils(DefaultPartition);
- using (DirectoryEntry root = adUtils.GetDefaultPartition())
- using (DirectoryEntry user = adUtils.FindUserByAccountName(_shortName))
- {
- byte[] thumbnailPhotoBytes = (byte[])user.Properties["thumbnailPhoto"].Value;
- bool hasThumbnailPhoto = (thumbnailPhotoBytes != null);
- ADImage.Visible = hasThumbnailPhoto;
- ADImageLabel.Visible = !hasThumbnailPhoto;
- if (hasThumbnailPhoto)
- {
- using (SPSite site = new SPSite(GetMySiteHostUrl(SPContext.Current.Site)))
- {
- site.AllowUnsafeUpdates = true;
- using (SPWeb web = site.OpenWeb())
- {
- web.AllowUnsafeUpdates = true;
- ProfileImagePicker profileImagePicker = new ProfileImagePicker();
- InitializeProfileImagePicker(profileImagePicker, web);
- SPFolder subfolderForPictures = GetSubfolderForPictures(profileImagePicker);
- UploadPhoto(_accountName, thumbnailPhotoBytes, subfolderForPictures);
- SetPictureUrl(_accountName, subfolderForPictures);
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- Warning.Visible = true;
- }
- RefreshControls();
- }
Helper methods in the AD2SP_Click method (UploadPhoto, SetPictureUrl, GetMySiteHostUrl etc.) are similar to the ones used in my former solutions here and here.
Active Directory related code is included in the ADUtils class (see credits later):
- public class ADUtils
- {
- public ADUtils(String defaultPartition)
- {
- DefaultPartition = defaultPartition;
- }
- private String DefaultPartition
- {
- get;
- set;
- }
- /// <summary>
- /// Retrieves a DirectoryEntry using configuration data
- /// </summary>
- /// <param name="path"></param>
- /// <returns></returns>
- public DirectoryEntry CreateDirectoryEntry(string path)
- {
- return new DirectoryEntry(String.Format("LDAP://{0}", path));
- }
- /// <summary>
- /// Creates a DirectoryEntry from the DefaultPartition defined in config
- /// </summary>
- /// <returns></returns>
- public DirectoryEntry GetDefaultPartition()
- {
- return CreateDirectoryEntry(DefaultPartition);
- }
- /// <summary>
- /// Simple method to find and return a user using the CN name and searching the
- /// defaultNamingContext defined in config.
- /// </summary>
- /// <param name="userRDN"></param>
- /// <returns></returns>
- public DirectoryEntry FindUserByCN(string userRDN)
- {
- using (DirectoryEntry searchRoot = GetDefaultPartition())
- {
- DirectorySearcher ds = new DirectorySearcher(
- searchRoot,
- String.Format("(cn={0})", userRDN),
- new string[] { "cn" },
- SearchScope.Subtree
- );
- SearchResult sr = ds.FindOne();
- return (sr != null) ? sr.GetDirectoryEntry() : null;
- }
- }
- /// <summary>
- /// Simple method to find and return a user using the sAMAccountName name and searching the
- /// defaultNamingContext defined in config.
- /// </summary>
- /// <param name="userRDN"></param>
- /// <returns></returns>
- public DirectoryEntry FindUserByAccountName(string accountName)
- {
- Trace.TraceInformation("FindUserByAccountName method called. Account name: '{0}'", accountName);
- using (DirectoryEntry searchRoot = GetDefaultPartition())
- {
- DirectorySearcher ds = new DirectorySearcher(
- searchRoot,
- String.Format("(sAMAccountName={0})", accountName),
- new string[] { "sAMAccountName" },
- SearchScope.Subtree
- );
- SearchResult sr = ds.FindOne();
- Trace.TraceInformation("FindUserByAccountName user found. Path: '{0}'", sr.Path);
- return (sr != null) ? sr.GetDirectoryEntry() : null;
- }
- }
- }
Note: The sample web part provided “as is”, and is intended to be used only as a proof of concept.
Important point on security: The sample should work only when at least two of the followings are located on the same computer: browser, SharePoint 2010 front end, Active Directory domain controller. If all of these three are located on a different computer and there is no Kerberos implemented then you should be prepared for the so-called double hops issue.
My general recommendation for that problem is accessing the resource (in this case the AD) using a service user account that has access to all the requested information. One can store the service account credentials for example in SSO or in an encrypted configuration parameter.
Implementing this features require some modifications in the current code. For example, you have to use a DirectoryEntry constructor in the CreateDirectoryEntry method of ADUtils class that has user name and password parameters.
You will not have such issues if you alter the solution to a server process, like timer job that runs as a dedicated user account.
Credits: I would like to say thanks to Joe Kaplan and Ryan Dunn for their book The .NET Developer’s Guide to Directory Services Programming. The book and the code samples are definitely a must for every .NET programmer who wants (or has) to dive into the beauty of Active Directory programming. The ADUtils class in my sample is based on a solution found in the book.
Hey,
Thanks for providing me the link. I have published your comment on my website. This is really good work. I appreciate your effort
Thanks
Chaitu
Comment by Chaitu — August 10, 2010 @ 13:44
I’m new to SP2010 and uploading web parts. I’ve browsed to, and selected ProfilePhotoSyncWebPart.webpart. I can now see it under the Imported Web Parts category but when I try to Add it, I get the following message:
$Resources:core,ImportErrorMessage;
Any ideas? Do I need to copy the contents of the zip files somewhere?
Thank you.
Comment by Rory Schmitz — September 15, 2010 @ 18:41
Hi,
why don’t just let the user add a picture on his Profile Page under My Sites, and add an export connection for the Picture property to thumbnailPhoto?
Comment by audun — October 8, 2010 @ 09:40
[...] Active Directory – SharePoint profile user image synchronization By Peter Holpar In my former post I’ve illustrated how to synchronize user images between AD and SharePoint using a custom web part [...]
Pingback by Creating a timer job for Active Directory – SharePoint profile user image synchronization « Second Life of a Hungarian SharePoint Geek — November 17, 2010 @ 02:40
Howcome you dont use the OOB PowerShell cmdlet to create the thumbnails in your User Photos librart on my site host?
Update-SPProfilePhotoStore -MySiteHostLocation http://my -CreateThumbnailsForImportedPhotos $true
The thumbnail parameter was added in a later CU (not sure which)
Comment by Anders Rask — May 17, 2011 @ 12:17
Hi Anders,
Well, there are several reasons for that. First, if you check the date of this post that is August, and the method you suggested requires the October Cumulative Update (at least, based on the info I found here
Import User Profile Photos from Active Directory into SharePoint 2010).
Second, as you can see, the method described here enables two-way synchronization between AD and SharePoint, not import only. AFAIS this cmdlet using the parameters as you suggested does nothing more than generates the thumbnails that are imported as part of the OOB AD to SP user profile synchronization.
If you read my posts here you see I primarily work with custom code development, and the method described in this post enables creating custom solutions. Furthermore, the web part enables self service for users to upload and change their photo from the web browser, typically not the case for PowerShell cmdlets. In the beginning of the post I wrote exactly that my goal was to create a tool that makes it possible for users to set their Active Directory / Outlook photo from a web UI. Your comment would probably apply better to my former post that is after all about creating a custom command line admin tool (see it here: How to upload a user profile photo programmatically), although I feel my post is still a better example to upload programatically (as its title says) than the one that uses a cmdlet, that I feel rather an admin tool. To tell the truth, I don’t see how one could use the cmdlet to upload images from the file system (or from any arbitrary byte stream source, like SQL, or the binary thumbnailPhoto AD property ) however using the code illustrated in my former post it is possible.
And last but not least, I was not aware of this PowerShell cmdlet, so thanks for sharing that with the readers and me. If my time allows, I will check the source of the cmdlet to see how it work. Hopefully I will learn new techniques from that.
Peter
Comment by Peter Holpar — May 17, 2011 @ 16:33
Excellent app, works a treat.. I have a question though that was raised by HR and IT
If I need to for any reason delete all the thumbnailPhoto for every user in AD how could I go about that ?
Comment by dan — May 26, 2011 @ 02:24
Hi Dan,
In this case you should iterate through all users in AD and reset the thumbnailPhoto property for each. You can find useful code snippets in my other post here: Creating a timer job for Active Directory – SharePoint profile user image synchronization.
Peter
Comment by Peter Holpar — May 26, 2011 @ 08:12
Any idea if this works in SharePoint online?
Comment by Shawn — August 31, 2012 @ 15:51