SharePoint User Profile Synchronization: Photos from AD to On-Prem Using PowerShell

06 Feb, 2024 | 5 minutes read

Introduction

Active Directory (AD) is a commonly used directory service for managing users and their attributes, including profile pictures. On the other hand, SharePoint, a product by Microsoft, is a popular collaboration platform utilized for document management, intranets, and team sites. While SharePoint supports importing user profiles from AD using Active Directory Import, picture synchronization is not supported out of the box. In this blog post, we will explore how Microsoft’s PowerShell can be leveraged to bridge this gap and enable the profile synchronization of pictures from AD DS to SharePoint on-premises. This necessitates the use of a custom PowerShell script to overcome the default User Profile Service, functionalities’ limitations, ensuring a smooth synchronization process for users’ picture.


Why Sync Pictures from AD to SharePoint?

Profile pictures provide a visual identity to users within an organization and are displayed across various collaboration platforms. By synchronizing pictures from AD to SharePoint, organizations can ensure consistency in user profiles and improve the overall user experience. Additionally, this synchronization can save time and effort for both administrators and end-users by automating the picture update process.

Search for SharePoint Farm Account

Requirements

Before proceeding with the PowerShell script, ensure that you have the following items:

  1. The active Directory module for PowerShell is installed on the machine running the script.
  2. SharePoint Server Subscription Edition deployed on-premises. (or any other from 2013)
  3. Sufficient permissions to read and modify user attributes in AD and SharePoint; we are using a farm account in this case.

The PowerShell Script: Picture Sync Process

The PowerShell script provided below outlines a step-by-step process to synchronize user profile pictures from Active Directory to SharePoint on-premises:

  • “CombineUrls”
    • Helper function for creating URL mappings for user and group data in SharePoint, making interaction easier.
function CombineUrls([string]$baseUrl, [string]$relUrl){
    [System.Uri]$base = New-Object System.Uri($baseUrl, [System.UriKind]::Absolute);
    [System.Uri]$rel = New-Object System.Uri($relUrl, [System.UriKind]::Relative);
    return (New-Object System.Uri($base, $rel)).ToString();
}
  • “GetPictureFromAD”
    • This PowerShell function determines whether or not the user has a picture in SharePoint. If the user has yet to do so, the image for the supplied user is fetched from AD and stored in a local folder on a VM running SharePoint.
function GetPictureFromAD{
    param (
        [string]$IntranetURL,
        [string]$LocalFolderPath,
        [string]$logFilePath
    )
    if (-not $IntranetURL -or -not $logFilePath -or -not $LocalFolderPath) {
        Write-Host "Please provide values for all parameters."
        return
    }
    Import-Module ActiveDirectory 
    $site = Get-SPSite $IntranetURL
    $serviceContext = Get-SPServiceContext $site
    $userProfileManager = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager($serviceContext)
    $logonNamesWithoutPictures = @()
    foreach ($userProfile in $userProfileManager.GetEnumerator()) {
        if (!$userProfile["PictureURL"].Value) {
            $logonNamesWithoutPictures += $userProfile["AccountName"].Value
            $logonName = $userProfile["AccountName"].Value
            $logMessage = "User Logon Name without Picture: $logonName"
            Write-Host $logMessage
            Add-Content -Path $logFilePath -Value $logMessage
        }
    }
    $site.Dispose()
    foreach ($logonName in $logonNamesWithoutPictures) {
        $userWithoutDomain = $logonName -replace '^[^\\]*\\'
        $adUser = Get-ADUser -Filter {SamAccountName -eq $userWithoutDomain} -Properties SamAccountName, thumbnailPhoto
        if ($adUser) {
            Write-Host "User Logon Name found in AD: $($adUser.SamAccountName)"
            if ($adUser.thumbnailPhoto) {
                $picturePath = "$localFolderPath\$userWithoutDomain.jpg"
                [System.IO.File]::WriteAllBytes($picturePath, $adUser.thumbnailPhoto)
                Write-Host "Profile picture saved to: $picturePath"
            } 
            else{
                Write-Host "User does not have a profile picture in Active Directory"
            }            
        } 
        else{
            Write-Host "User Logon Name not found in AD: $userWithoutDomain"
        }
    }
}
  • “UploadADPicturesToSP”
    • This method checks if there are photos in the folder “Source,” and if there are, then each user picture in that folder is uploaded to Sharepoint, and a thumbnailPhoto is created for each image.
function UploadADPicturesToSP{
    param (
        [string]$UserProfilesURL,
        [string]$IntranetURL,
        [string]$PicLibraryName,
        [string]$LocalFolderPath,
        [string]$Domain
    )
    if (-not $UserProfilesURL -or -not $IntranetURL -or -not $PicLibraryName -or -not $LocalFolderPath -or -not $Domain) {
        Write-Host "Please provide values for all parameters."
        return
    }

    if((Get-PSSnapin | Where {$_.Name -eq "Microsoft.SharePoint.PowerShell"}) -eq $null) {
        Add-PSSnapin Microsoft.SharePoint.PowerShell;
    }

    $spNotFoundMsg = "Unable to connect to SharePoint.  Please verify that the site '$IntranetURL' is hosted on the local machine.";

    if ([Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") -eq $null)       { throw $spNotFoundMsg; }
    if ([Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server") -eq $null)    { throw $spNotFoundMsg; }
    
    $web = Get-SPWeb $UserProfilesURL
    $picFolder = $web.Folders[$picLibraryName]
    if(!$picFolder)
    {
        Write-Host "Picture Library Folder not found"
        return

    $files = ([System.IO.DirectoryInfo] (Get-Item $localFolderPath)).GetFiles() | ForEach-Object {
        $username = [IO.Path]::GetFileNameWithoutExtension($_.FullName);
        $fileStream = ([System.IO.FileInfo] (Get-Item $_.FullName)).OpenRead()
        $contents = new-object byte[] $fileStream.Length
        $fileStream.Read($contents, 0, [int]$fileStream.Length);
        $fileStream.Close();
        write-host "Copying" $_.Name "to" $picLibraryName "in" $web.Title "..."
        $userNameFromPic = $_.Name -replace '\.jpg$',''
        $spFile = $picFolder.Files.Add($picFolder.Url + "/" + $_.Name, $contents, $true)
        $spItem = $spFile.Item
        $up = $null;
        $serviceContext = Get-SPServiceContext $IntranetURL
        $userProfileManager = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager($serviceContext)
        $targetLogonName = $domain+"\"+$userNameFromPic;
        foreach ($userProfile in $userProfileManager.GetEnumerator()) {
            $logonName = $userProfile["AccountName"].Value
            if ($logonName -eq $targetLogonName) {
                $up = $userProfile
            }
        }
        $picturePropertyName = "PictureURL";
        if($up -ne $null)
        {
            if (-not [System.String]::IsNullOrEmpty($picturePropertyName))
            {
                $PortraitUrl = CombineUrls -baseUrl $spFile.Web.Url -relUrl $spFile.ServerRelativeUrl;
                Write-Host $PortraitUrl                             
                $up.get_Item($picturePropertyName).Value = $PortraitUrl;
                $up.Commit();
            }
        }
    }
    Write-Host "Updating User Profile Photo Store..." -foregroundcolor yellow
    Update-SPProfilePhotoStore –MySiteHostLocation $UserProfilesURL
    Write-Host "Done" -foregroundcolor green
    Update-SPProfilePhotoStore -CreateThumbnailsForImportedPhotos 1 -MySiteHostLocation $UserProfilesURL
    Write-Host "Done - Photo store and creating thumbnails" -foregroundcolor green
  • “CreateArchivePicToSpSync”
    • This method cleans the folder and creates an archive with a backup for every sync.
function CreateArchivePicToSpSync{
    param (
        [string]$folderToZip,
        [string]$archiveFolder
    )
    if (-not $folderToZip -or -not $archiveFolder) {
        Write-Host "Please provide values for all parameters."
        return
    }
    
    $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
    $zipFileName = "Archive_$timestamp.zip"
    $zipFilePath = Join-Path $archiveFolder $zipFileName
    Compress-Archive -Path $folderToZip -DestinationPath $zipFilePath
    Remove-Item -Path $folderToZip\* -Recurse -Force
    Write-Host "Zip file created and stored in $zipFilePath. Original folder is now empty."
  • “SyncImagesFromADtoSP”
    • This main function initializes all variables and calls other functions in the correct order.
function SyncImagesFromADtoSP(){

    #Init variable - provided variables are the default ones, if you have some another specificaiton just change it.
    #domain -> the domain of the AD
    $domain = "Domain"
    #UserProfilesURL -> sharepoint storage place for user profiles
    $UserProfilesURL = "https://mysites/"
    #IntranetURL -> sharepoint web application root url
    $IntranetURL = "https://intranet"
    #picLibraryName -> picture library where the user profile pictures are stored
    $picLibraryName = "User%20Photos"
    #localFolderPath -> Local folder where the photos are stored
    $localFolderPath = "C:\Users\farmUser\Desktop\Source"
    #logFilePath -> Destination for logFile   
    $logFilePath = "C:\Users\farmUser\Desktop\Source\Log.txt"
    #Location for the archive folder
    $archiveFolder = "C:\Users\farmUser\Desktop\Archive"

    GetPictureFromAD -IntranetURL $IntranetURL -LocalFolderPath $LocalFolderPath -logFilePath $logFilePath
    Start-Sleep -Seconds 30
    Upload-ADPicturesToSP -UserProfilesURL $UserProfilesURL -IntranetURL $IntranetURL -PicLibraryName $PicLibraryName -LocalFolderPath $LocalFolderPath -Domain $Domain
    Start-Sleep -Seconds 30
    CreateArchivePicToSpSync -folderToZip $folderToZip -archiveFolder $localFolderPath
    Write-Host "The photos are uploaded to Sharepoint" -foregroundcolor green
}

SyncImagesFromADtoSP

Usage and Configuration

To use the provided script, follow these steps:

  1. Open a PowerShell window with Administrator privileges.
  2. Copy and paste the script into the PowerShell window.
  3. Modify the ($domain, $UserProfilesURL, $IntranetURL, $picLibraryName, $localFolderPath, $logFilePath, $archiveFolder) variables with your links respectively.
  4. Execute the script using PowerShell.
  5. Schedule this script using the Windows task scheduler to maintain continuous sync. How to schedule a task on a windows machine

Conclusion

We can seamlessly sync profile pictures from AD to SharePoint on-premises by leveraging PowerShell. This blog post has provided a step-by-step PowerShell script to automate the process, saving time and ensuring consistency in user profile pictures.