Friday, 7 February 2020

SharePoint Online - Declare / Undeclare Item as a record using Powershell CSOM



A - OBJECTIVE: 

During process automation in SharePoint Online, Powershell script can be considered to programmatically declare (or undeclare) items and files as a record .

The manual steps that can be automated for multiple items are as follows:





B - ARCHITECTURE:

1. The site collection feature "In Place Records Management" must be activated; then, at each individual list / library settings (Record declaration settings), we need to configure the manual records management settings.




2. Field Value _vti_ItemHoldRecordStatus (int) can be used to identify the record status of the item (reference 3): 

  • _vti_ItemHoldRecordStatus = 0 : a normal document / item
  • _vti_ItemHoldRecordStatus = 273 : a record
  • _vti_ItemHoldRecordStatus = 4353: an item on hold


3. Field Value _vti_ItemDeclaredRecord (datetime) is set to the current date time soon after the associated item is set as a record; and this value is cleared once the item is undeclared as a record.

4. The functions DeclareItemAsRecord / UndeclareItemAsRecord (using ClientRuntimeContext) in the assembly Microsoft.SharePoint.Client.RecordsRepository.Records of the library Microsoft.Office.Client.Policy.dll is used to declare / undeclare a record:

#declare the record
if($DocItem.FieldValues["_vti_ItemHoldRecordStatus"] -eq 0)
{
[Microsoft.SharePoint.Client.RecordsRepository.Records]::DeclareItemAsRecord($clientRuntimeContext,$DocItem)
}

#undeclare the record
if($DocItem.FieldValues["_vti_ItemHoldRecordStatus"] -eq 273)
{
[Microsoft.SharePoint.Client.RecordsRepository.Records]::UndeclareItemAsRecord($clientRuntimeContext,$DocItem)
}

5. The declared item has a LOCK icon at the file type and the datetime value in the column Declared Record:





C - SOLUTION:

1. Powershell tool must be Windows Powershell or Windows Powershell ISE. The script does not work in SharePoint Online Management Shell at the moment (Microsoft.Online.SharePoint.PowerShell version 16.0.7414.0) , and it will throw an error:

Cannot convert argument "context", with value: "Microsoft.SharePoint.Client.ClientContext", for "IsRecord" to type "Microsoft.SharePoint.Client.ClientRuntimeContext": "Cannot convert the "Microsoft.SharePoint.Client.ClientContext" value of type "Microsoft.SharePoint.Client.ClientContext" to type "Microsoft.SharePoint.Client.ClientRuntimeContext"." ---> System.Management.Automation.PSInvalidCastException: Cannot convert the "Microsoft.SharePoint.Client.ClientContext" value of type "Microsoft.SharePoint.Client.ClientContext" to type "Microsoft.SharePoint.Client.ClientRuntimeContext".

2. SharePoint Online Client Components SDK (link here): the latest version should be installed and referenced to in the Powershell script:

Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll"

Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"


Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.Office.Client.Policy.dll"

3. DLL version must be 16+ (i.e. the version 15.x is not recommended)




4. The function IsRecord does not return a proper value as expected in this DLL version; hence, we should use the field value $DocItem.FieldValues["_vti_ItemHoldRecordStatus"]

[Boolean] $isRecord = [Microsoft.SharePoint.Client.RecordsRepository.Records]::IsRecord($clientRuntimeContext,$DocItem)

D - SOURCE CODE:


#region Load SharePoint Client Assemblies
Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.Office.Client.Policy.dll"
#endregion

# Document Library
$libRequestDocs = "Request Documents"

# Connection Credetials 
$UserName = "admin@nhn.onmicrosoft.com"
$PASSWORD = "YourPassword"
$SecurePassword = ConvertTo-SecureString -String $PASSWORD -AsPlainText -Force

# Target environment
$SiteUrl = "https://nhn.sharepoint.com/sites/records"

# Global variables
$archivedIDs = [System.Collections.ArrayList]@()

### Connection to SP Online Archive subsites to get the reference
Try
{
    #connect/authenticate to SharePoint Online and get ClientContext object..        
    $clientContextArchive = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl) 
 
 [Microsoft.SharePoint.Client.ClientRuntimeContext]$clientRuntimeContext=[Microsoft.SharePoint.Client.ClientRuntimeContext]$clientContextArchive.Site.Context
 
    $credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName, $SecurePassword) 
    $clientContextArchive.Credentials = $credentials
 
 $clientContextArchive.Load($clientContextArchive.Web)
 $clientContextArchive.ExecuteQuery()
  
    Write-Host "Connected to SharePoint Online site: " $SiteUrl"/Archive001/" -ForegroundColor Green
}
Catch
{
    $SPOConnectionException = $_.Exception.Message
    Write-Host ""
    Write-Host "Connection Error:" $SPOConnectionException -ForegroundColor Red
    Write-Host ""
    Break
}

Try {
    #Get the Document Library
    $libDocs =$clientContextArchive.Web.Lists.GetByTitle($libRequestDocs)
    $clientContextArchive.Load($libDocs)
 $clientContextArchive.ExecuteQuery()
 
    #Define CAML Query to Get All Files    
    $queryCommand = " 
  
   
  
  1000
 "
 
    $DataCollection = @() 
 $Count = 1;
 $position = $null
 
 $queryCAML = New-Object Microsoft.SharePoint.Client.CamlQuery
 $queryCAML.ViewXml = $queryCommand
 
 #handle large library with more than 5000 items
 Do
 {
  Try{
   #powershell sharepoint online list all documents
   $currentDocCollection = $libDocs.GetItems($queryCAML)
   $clientContextArchive.Load($currentDocCollection)
   $clientContextArchive.ExecuteQuery()
   
   $queryCAML.ListItemCollectionPosition = $currentDocCollection.ListItemCollectionPosition 
   
   Write-host -f Green "Collection Index " $currentDocCollection.ListItemCollectionPosition.pagingInfo 
   
   #Iterate through each document in the library
   ForEach($DocItem in $currentDocCollection)
   {
    $clientContextArchive.Load($DocItem)
    $clientContextArchive.Load($DocItem.File)
    $clientContextArchive.ExecuteQuery()
    
    #this function does not return expected results
    [Boolean] $isRecord = [Microsoft.SharePoint.Client.RecordsRepository.Records]::IsRecord($clientRuntimeContext,$DocItem)
    $clientContextArchive.ExecuteQuery()
    
    Write-host -f Green "Doc:" $DocItem.FieldValues
    Write-host -f Green "[Record Details] _vti_ItemHoldRecordStatus: " $DocItem.FieldValues["_vti_ItemHoldRecordStatus"]
    Write-host -f Green "[Record Details] _vti_ItemDeclaredRecord: " $DocItem.FieldValues["_vti_ItemDeclaredRecord"]
    
    #undeclare the record
    if($DocItem.FieldValues["_vti_ItemHoldRecordStatus"] -eq 273){
     [Microsoft.SharePoint.Client.RecordsRepository.Records]::UndeclareItemAsRecord($clientRuntimeContext,$DocItem)

     try
     {
      $clientRuntimeContext.ExecuteQuery()  
      Write-Host "...Item has been undeclared a record" -ForegroundColor Green
      Write-host -f Blue "Doc:" $DocItem.FieldValues
     }
     catch
     {
      Write-Host "...Item could not be undeclared a record" -ForegroundColor Red
      write-host -f Red "Error Level 3:" $_.Exception
     }       
    }
    
    #declare the record
    if($DocItem.FieldValues["_vti_ItemHoldRecordStatus"] -eq 0){
     [Microsoft.SharePoint.Client.RecordsRepository.Records]::DeclareItemAsRecord($clientRuntimeContext,$DocItem)

     try
     {
      $clientRuntimeContext.ExecuteQuery()  
      Write-Host "...Item has been declared a record" -ForegroundColor Green
      Write-host -f Blue "Doc:" $DocItem.FieldValues
     }
     catch
     {
      Write-Host "...Item could not be declared a record" -ForegroundColor Red
      write-host -f Red "Error Level 3:" $_.Exception
     }       
    }   
   }
  }
  Catch{
   write-host -f Red "Error Level 2:" $_.Exception
  }
 }
 While($queryCAML.ListItemCollectionPosition -ne $null)
}
Catch {
    write-host -f Red "Error Level 1:" $_.Exception.Message
}

REFERENCE:

1. https://gallery.technet.microsoft.com/office/SharePoint-Online-f6bae254/view/Discussions#content

2. https://www.sharepointpals.com/post/check-whether-a-document-is-already-a-record-or-not-isrecord-in-sharepoint-office-365-programmatically-using-c-csom/


3. A sample of a file's field values (or properties):


[ID, 1638][ContentTypeId, 0x010100A5FD4705EF695745935DCFF362D96FD9002FFDDFEEE3A428488245399665244602][Created, 31/01/2020 3:55:24 AM][Author, Microsoft.SharePoint.Client.FieldUserValue][Modified, 31/01/2020 3:55:24 AM][Editor, Microsoft.SharePoint.Client.FieldUserValue][_HasCopyDestinations, ][_CopySource, ][_ModerationStatus, 3][_ModerationComments, ][FileRef, /sites/ourbriefings-test/Archive001/RequestDocuments/BAC-100/ArchiveFlagNHN-2020-01-31.txt][FileDirRef, /sites/ourbriefings-test/Archive001/RequestDocuments/BAC-100][Last_x0020_Modified, 2020-01-31T03:55:24Z][Created_x0020_Date, 2020-01-31T03:55:24Z][File_x0020_Size, 11228][FSObjType, 0][SortBehavior, Microsoft.SharePoint.Client.FieldLookupValue][CheckedOutUserId, Microsoft.SharePoint.Client.FieldLookupValue][IsCheckedoutToLocal, 0][CheckoutUser, Microsoft.SharePoint.Client.FieldUserValue][FileLeafRef, ArchiveFlagNHN-2020-01-31.txt][UniqueId, df9fe88c-4a14-47ca-a8ae-20bbcfc2a680][SyncClientId, Microsoft.SharePoint.Client.FieldLookupValue][ProgId, ][ScopeId, {8024A1D9-532B-4461-BF67-F2A53DE8A3B9}][VirusStatus, Microsoft.SharePoint.Client.FieldLookupValue][CheckedOutTitle, Microsoft.SharePoint.Client.FieldLookupValue][_CheckinComment, ][Modified_x0020_By, i:0#.f|membership|anthony.nguyen@an.com][Created_x0020_By, i:0#.f|membership|anthony.nguyen@an.com][File_x0020_Type, txt][HTML_x0020_File_x0020_Type, ][_SourceUrl, ][_SharedFileIndex, ][MetaInfo, SendEmailToAuthors:SW|Send Emailvti_parserversion:SR|16.0.0.19708vti_previewinvalidtime:TX|31 Jan 2020 03:55:22 -0000vti_author:SR|i:0#.f|membership|anthony.nguyen@an.comvti_dbschemaversion:SR|16.0.204.0vti_sprocsschemaversion:SR|16.0.452.0CBSStatus:SW|DraftIconOverlay:SW||txt|lockoverlay.pngecm_ItemDeleteBlockHolders:SW|ecm_InPlaceRecordLockvti_writevalidationtoken:SW|GQIq0B5WkD/F3Wg7yUVPfqvtYLk=DocumentSetDescription:SW|vti_modifiedby:SR|i:0#.f|membership|anthony.nguyen@an.comecm_RecordRestrictions:SW|BlockDelete, BlockEditvti_foldersubfolderitemcount:IR|0ContentTypeId:SW|0x010100A5FD4705EF695745935DCFF362D96FD9002FFDDFEEE3A428488245399665244602ecm_ItemLockHolders:SW|ecm_InPlaceRecordLockvti_folderitemcount:IR|0_vti_ItemDeclaredRecord:SW|2020-01-30T19:56:26Z_vti_ItemHoldRecordStatus:IW|273HideFromDelve:BW|true][_Level, 2][_IsCurrentVersion, True][ItemChildCount, 0][FolderChildCount, 0][Restricted, ][OriginatorId, ][NoExecute, 0][ContentVersion, 1][_ComplianceFlags, ][_ComplianceTag, ][_ComplianceTagWrittenTime, ][_ComplianceTagUserId, ][BSN, Microsoft.SharePoint.Client.FieldLookupValue][_ListSchemaVersion, ][_Dirty, 0][_Parsable, 0][_StubFile, 0][AccessPolicy, 0][_VirusStatus, ][_VirusVendorID, ][_VirusInfo, ][_CommentFlags, ][_CommentCount, ][_LikeCount, ][_RmsTemplateId, ][_IpLabelId, ][_DisplayName, ][_IpLabelAssignmentMethod, ][AppAuthor, ][AppEditor, ][SMTotalSize, Microsoft.SharePoint.Client.FieldLookupValue][SMLastModifiedDate, 2020-01-31T03:56:27Z][SMTotalFileStreamSize, 11228][SMTotalFileCount, Microsoft.SharePoint.Client.FieldLookupValue][owshiddenversion, 2][_UIVersion, 1][_UIVersionString, 0.1][InstanceID, ][Order, 163800][GUID, f5cb7fe8-1bc2-4a25-b164-614d2feb94d4][WorkflowVersion, 1][WorkflowInstanceID, ][ParentVersionString, Microsoft.SharePoint.Client.FieldLookupValue][ParentLeafName, Microsoft.SharePoint.Client.FieldLookupValue][DocConcurrencyNumber, 2][ParentUniqueId, {E14CB12D-6970-45C6-8452-8B2C50A6F25F}][StreamHash, 0x0219022AD01E56903FC5DD683BC9454F7EABED60B9][ComplianceAssetId, ][Title, ][TemplateUrl, ][xd_ProgID, ][xd_Signature, ][_ShortcutUrl, ][_ShortcutSiteId, ][_ShortcutWebId, ][_ShortcutUniqueId, ][CBSStatus, Draft][CBSDocComments, ][CBSDueBy, ][CBSDocType, ][CBSFileName, ][CBSReviewersContributors, ][SendEmailToAuthors, Send Email][RequestID, ][DocumentSetDescription, ][Send_x0020_Email_x0020_To_x0020_Reviewer, ][Update_x0020_Request_x0020_Number, ][UpdateDo, ][UpdateNe, ][MediaServiceMetadata, ][MediaServiceFastMetadata, ][Send_x0020_Email_x0020_To_x0020_Reviewer_x0028_1_x0029_, ][Update_x0020_Request_x0020_number0, ][UpdateDo0, ][UpdateNe0, ][MediaServiceAutoTags, ][MediaServiceOCR, ][Document_x0020_Status, ][UploadedBy, ][CBSDocumentType, ][CBSAuthor, ][CBSComments, ][CBSDocStatus, ][CBSDocDueDate, ][CBSSendToAuthor, ][CBSRepeaterDocID, ][FileUpdate, ][MediaServiceDateTaken, ][SharedWithUsers, ][SharedWithDetails, ][MediaServiceGenerationTime, ][MediaServiceEventHashCode, ][RequestTitle, ][TaskDueDate, ][Status, ][MediaServiceLocation, ][HideFromDelve, True][RequestNumber, ][RequestNumber_x003a_Current_x0020_Status, ][_vti_ItemDeclaredRecord, 30/01/2020 8:56:26 AM][_vti_ItemHoldRecordStatus, 273]