My Apps on the SharePoint Store

My Books

  • Apps
  • Azure
  • REST
  • CSOM/JSOM
  • Service Apps
  • WCF
  • REST/OData
  • CSOM/JSOM
  • SharePoint API
  • Silverlight
  • jQuery

Recent blog posts

User login

Home | Blogs | Stephane Eyskens's blog

Enterprise Services Farm and separate My Sites gotcha when synchronizing the picture from AD

Hi,

If you implement the following architecture (extracted from Technet at http://technet.microsoft.com/en-us/library/cc560988.aspx ) :

with the User Profile Service application hosted on a different farm than the My Sites and without using the replicator engine, you will encounter a problem with the Profile Picture synchronization if you get the picture from Active Directory.

The reason is due to the fact that when synchronizing, the UPS will try to upload the AD pictures into the User Photos picture library of the My Site host. Of course, in the above scenario, this site collection is located in a remote farm and isn't accessible via the API. The system could have dealt with that by using the web services or the FRPC but it's not the case, it's using the regular APIs which result in the following error (extracted from the ULS logs):

03/26/2012 20:48:59.20 	miiserver.exe (0x2830) User Profiles High    	
SavePictureToLibrary: Error processing the photo URL 0c37852b-34d0-418e-91c6-2ac25af4be5b_20146.jpg for user 20146: 
System.IO.FileNotFoundException: The Web application at http://demo2010aa:27371/ could not be found.
 Verify that you have typed the URL correctly. 

As you can see, FIM (miiserver.exe) encounters that error while synchronizing.

I made a PowerShell cmdlet that workarounds this problem by:

  • Connecting to AD to read the thumbnailPhoto attribute for all the objects of type person.
  • Handles both full and incremental imports
  • Connects to the remote My Site via the FRPC which is much faster then the web services.
  • Updates each profile with the new picture URL

This cmdlet takes about 5 minutes/1000 users or faster depending on your hardware power. After this has run, you still need to run the OOTB cmdlet Update-SPProfilePhotoStore as shown below:

Of course, when using this cmdlet, you can remove the mapping in the UPS for the Picture property so that your sync does not fail anymore.

Here is how it's implemented :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint.PowerShell;
using Microsoft.Office.Server.UserProfiles;
using System.Net;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using System.DirectoryServices;
using System.Management.Automation;

namespace SPImport_ADPicture
{
    
    /*---------------------DISCLAIMER---------------------------------------------
 * Note that this software is delivered as is with no warranty of any kind. Use it
 * at your own risks.
 * 
 * Credits : Silver-IT Stephane Eyskens
 * Site : http://www.silver-it.com
 * 
 * 
 * Feel free to customize the source code! Happing Coding!
 * */
    
    public class SPImportADPictureCode : SPCmdlet
    {
        #region parameters
        [Parameter(Mandatory = true, HelpMessage = "AD Page Size")]
        public int ADPageSize { get; set; }

        [Parameter(Mandatory = false, HelpMessage = "Incremental Import")]
        public string IncrementalStartDate { get; set; }
        #endregion

        #region Helper Methods
        void UploadPicture(string url, string picturename, byte[] bytes)
        {
            string PutOption = "overwrite,createdir,migrationsemantics";
            string method = String.Format("method=put+document%3a12.0.4518.1016&service_name=%2f&document=[document_name={0};meta_info=[{1}]]&put_option={2}&comment={3}&keep_checked_out={4}\n", picturename, null, PutOption, null, "false");
            List<byte> data = new List<byte>();
            data.AddRange(Encoding.UTF8.GetBytes(method));
            data.AddRange(bytes);

            using (WebClient FRPCClient = new WebClient())
            {
                FRPCClient.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
                FRPCClient.Headers.Add("Content-Type", "application/x-vermeer-urlencoded");
                FRPCClient.Headers.Add("X-Vermeer-Content-Type", "application/x-vermeer-urlencoded");
                string result = Encoding.UTF8.GetString(FRPCClient.UploadData(url + "/_vti_bin/_vti_aut/author.dll", "POST", data.ToArray()));
                if (result.IndexOf("\n<p>message=successfully") < 0)
                    throw new ApplicationException(result);
            }
        }

        void ShowErrorAndExit(string error)
        {
            
            WriteResult(error);
            Environment.Exit(2);

        }
        #endregion

        #region implementation
        protected override void InternalProcessRecord()
        {
            string MySiteHostUrl = null;
            UserProfileManager UpMgr = null;           
            
            WriteResult(Messages.cProgramStart);

            try
            {
                UpMgr = new UserProfileManager(
                    SPServiceContext.GetContext(SPServiceApplicationProxyGroup.Default, SPSiteSubscriptionIdentifier.Default));
                MySiteHostUrl = UpMgr.MySiteHostUrl;                
                WriteResult(string.Format(Messages.cMySiteHost, MySiteHostUrl));
            }
            catch (Exception Ex)
            {
                ShowErrorAndExit(string.Format(Messages.cUPSError, Ex.Message));
            }


            try
            {

                DirectorySearcher ADSearch = new DirectorySearcher();
                //Getting all the users having a picture defined in the thumbnailPhoto attribute (full or incremental import).
                ADSearch.Filter = (!String.IsNullOrEmpty(IncrementalStartDate)) ?
                    string.Format("(&(objectClass=user)(objectCategory=person)(thumbnailPhoto=*)(|(whenChanged>={0})(whenCreated>={0})))",
                    string.Concat(IncrementalStartDate, "010000.0-0500")) :
                    "(&(objectClass=user)(objectCategory=person)(thumbnailPhoto=*))";

                ADSearch.PageSize = ADPageSize;
                ADSearch.PropertiesToLoad.Add(Constants.cSamAccountName);
                ADSearch.PropertiesToLoad.Add(Constants.cThumbnailPhoto);

                SearchResultCollection ADResults = ADSearch.FindAll();
               
                if (ADResults != null)
                {
                    //   Logger.Information(LoggingCategory.Console,string.Format(Messages.cADCount,ADResults.Count));
                    WriteResult(string.Format(Messages.cADCount, ADResults.Count));
                    for (int counter = 0; counter < ADResults.Count; counter++)
                    {
                        SearchResult result = ADResults[counter];
                        if (result.Properties.Contains(Constants.cThumbnailPhoto))
                        {
                            string AccountName = result.Properties[Constants.cSamAccountName][0] as string;
                            byte[] PictureFile = result.Properties[Constants.cThumbnailPhoto][0] as byte[];

                            Dictionary<string, object> meta = new Dictionary<string, object>();

                            try
                            {
                                UserProfile p = UpMgr.GetUserProfile(AccountName);
                                p[Constants.cPictureUrlField].Value =
                                    string.Concat(
                                                    MySiteHostUrl,
                                                    Constants.cPictureLibPath,
                                                    AccountName,
                                                    Constants.cPictureExtension);
                                p.Commit();


                                UploadPicture(
                                    MySiteHostUrl,
                                    string.Concat(
                                        Constants.cPictureLibPath,
                                        AccountName,
                                        Constants.cPictureExtension),
                                    PictureFile);

                                WriteResult(string.Format(Messages.cProcessedADObject, AccountName));
                            }
                            catch (UserNotFoundException)
                            {
                                //this error is not fatal, if the user is not found, we still
                                //process the other users.
                                WriteResult(string.Format(Messages.cUserNotFound, AccountName));
                            }
                            catch (ApplicationException AppEx)
                            {
                                ShowErrorAndExit(string.Format(
                                                    Messages.cUploadError,
                                                    result.Properties[Constants.cSamAccountName][0], AppEx.Message));
                            }
                            catch (Exception Ex)
                            {
                                ShowErrorAndExit(string.Format(
                                                    Messages.cUserGenericError,
                                                    result.Properties[Constants.cSamAccountName][0], Ex.Message));
                            }
                        }
                    }

                }
            }
            catch (Exception Ex)
            {
                ShowErrorAndExit(string.Format(
                                    Messages.cGenericError,
                                    Ex.Message));
            }
        }
        #endregion
    }
}

So the code basically performs a query to AD (must be in the same domain here but feel free to modify the adjust the code to your needs), uploads the profile picture to the remote mysite using the FRPC (this protocol must be enabled on the my site farm which is the case by default) and adjusts the user profiles accordingly. Afterwards, just running the OOTB cmdlet finishes the work.

Here is the link to the Source Code
Here is the link to the Solution Only

Happy Coding!

Comments

Nice script

Thank you Stephane for this script!