Resetting all users’ start menu for Office 2019

With the plan to move to Office 2019, I wanted to be able automatically reset the start menu for all users on a computer. In my powershell script that installs Office 2019, I also have it search all local users profiles for their start menu layout and replace old Office 2016 entries with Office 2019.

If ($null -eq (Get-PSDrive HKU -ErrorAction SilentlyContinue)) { New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS | Out-Null }
$LoggedOnUsers = (Get-CimInstance -Class Win32_LoggedOnUser | Select-Object Antecedent -Unique).Antecedent.Name
$HiveList = "HKLM:\SYSTEM\CurrentControlSet\Control\hivelist"
$Hives = Get-Item -Path $HiveList | Select-Object -ExpandProperty Property | Where-Object {$_ -like "*\REGISTRY\USER\S-*" -And $_ -notlike "*_Classes*"}
$Users = (Get-ChildItem $env:SystemDrive\Users -Force -Exclude 'All Users','Public' | Where-Object {$_.PSIsContainer}).Name
ForEach ($Name in $Users) {
	$ntuserpath = "$env:SystemDrive\Users\$Name\ntuser.dat"
	If (Test-Path $ntuserpath) {
		$ntuserpath = '"' + $ntuserpath + '"'
		$RegPath = $null
		If ($LoggedOnUsers -like "*$Name*") {
			# User is logged in, will get HKU path
			ForEach ($Hive in $Hives) {
				$HiveValue = (Get-ItemPropertyValue -Path "$HiveList" -Name "$Hive") -Replace "\\Device\\HarddiskVolume[0-9]*","$env:SystemDrive"
				If ($HiveValue -like "*\$Name\*") { $RegPath = $Hive.ToUpper().Replace("\REGISTRY\USER","HKU"); break }
			}
		}
		If ($null -eq $RegPath) { $RegPath = "HKLM\ntuser"; $runreg = Start-Process -FilePath REG.exe -ArgumentList "LOAD $RegPath $ntuserpath" -PassThru -Wait -WindowStyle Hidden }
		$PSRegPath = $RegPath.Replace("\",":\")
		
		# Reset TileStore for Start Menu refresh
		If ($Name -notlike "Default*") {
			$LocalLayout = "$env:SystemDrive\Users\$Name\AppData\Local\Microsoft\Windows\Shell\LayoutModification.xml"
			If ((Test-Path "$LocalLayout") -And ($FixOfficeLayout)) {
				(Get-Content -Path "$LocalLayout").Replace("{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}\Microsoft Office\Office16\WINWORD.EXE","Microsoft.Office.WINWORD.EXE.15") | Set-Content "$LocalLayout"
				(Get-Content -Path "$LocalLayout").Replace("{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}\Microsoft Office\Office16\EXCEL.EXE","Microsoft.Office.EXCEL.EXE.15") | Set-Content "$LocalLayout"
				(Get-Content -Path "$LocalLayout").Replace("{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}\Microsoft Office\Office16\POWERPNT.EXE","Microsoft.Office.POWERPNT.EXE.15") | Set-Content "$LocalLayout"
				(Get-Content -Path "$LocalLayout").Replace("{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}\Microsoft Office\Office16\MSPUB.EXE","Microsoft.Office.MSPUB.EXE.15") | Set-Content "$LocalLayout"
				(Get-Content -Path "$LocalLayout").Replace("{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}\Microsoft Office\Office16\ONENOTE.EXE","Microsoft.Office.ONENOTE.EXE.15") | Set-Content "$LocalLayout"
				(Get-Content -Path "$LocalLayout").Replace("{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}\Microsoft Office\Office16\OUTLOOK.EXE","Microsoft.Office.OUTLOOK.EXE.15") | Set-Content "$LocalLayout"
				(Get-Content -Path "$LocalLayout").Replace("Microsoft.Office.OUTLOOK.EXE.16","Microsoft.Office.OUTLOOK.EXE.15") | Set-Content "$LocalLayout"
				(Get-Content -Path "$LocalLayout").Replace('Version="2"','Version="1"') | Set-Content "$LocalLayout"
				$RegTileStore = Get-ChildItem -Path "$PSRegPath\SOFTWARE\Microsoft\Windows\CurrentVersion\CloudStore\Store\Cache\DefaultAccount" -ErrorAction SilentlyContinue | Where-Object {$_.Name -like "*start.tilegrid*curatedtilecollection*"} | Select -ExpandProperty Name -First 1
				If ($null -ne $RegTileStore) {
					$RegTileStore = $RegTileStore.Substring($RegTileStore.IndexOf("\DefaultAccount\")+16)
					Remove-Item -Path "$PSRegPath\SOFTWARE\Microsoft\Windows\CurrentVersion\CloudStore\Store\Cache\DefaultAccount\$($RegTileStore)" -Force -Recurse -ErrorAction SilentlyContinue
				}
			}
			If ($RegPath -eq "HKLM\ntuser") {
				[gc]::collect()
				$runreg = Start-Process -FilePath REG.exe -ArgumentList "UNLOAD $RegPath" -PassThru -Wait -WindowStyle Hidden; Write-Output "$($runreg.ExitCode)"
			}
		}
	}
} # End of ForEach Users

This searches for all basic Office 2016 application entries in LayoutModification.xml and puts in the Office 2019 entries. It then clears out the registry key that will force the start menu refresh when each user logs back in.

ConfigMgr: -RemoveDetectionClause with Set-CM(Script/Msi)DeploymentType

Note April 3, 2019: CM1902 has been released with a fix to properly handle version numbers for detection clauses.

Note December 7, 2018:  Using New-CMDetectionClauseWindowsInstaller with ExpectedValue (ProductVersion) seems to work, but the final xml data shows the version number is a string instead. For .msi detections, I end up having to go in and manually browse for the file and set up the detection that way. Hoping it’ll be fixed in CM1902.

I began writing PowerShell scripts to help automate updating/replacing applications in ConfigMgr. About half of the programs are .exe with PowerShell detection and the other half are .msi using windows installer detection. Microsoft’s current documentation for removing detection clauses just has [-RemoveDetectionClause <String[]>] without any extra information.

Testing the command (Set-CMScriptDeploymentType -ApplicationName “appname” -DeploymentTypeName “apptitle” -RemoveDetectionClause “blahblah”) returns a warning message: “WARNING: Detection clause with a logical name of ‘blahblah’ was not found and will be ignored for removal.” I then looked into the deployment types SDMPackageXML and found two different id types. One called ‘LogicalName’ and the other called ‘SettingLogicalName’. Manually tested both entries and the SettingLogicalName id worked.

# Get application and deployment type title
$AppName = "Google Chrome"
$AppTitles = (Get-CMApplication -ApplicationName "$($AppName)" | ConvertTo-CMApplication).DeploymentTypes.Title
ForEach ($Title in $AppTitles) {
   $SDMPackageXML = (Get-CMDeploymentType -ApplicationName "$($AppName)" -DeploymentTypeName "$($Title)").SDMPackageXML

   # Regex to retrieve all SettingLogicalName ids. Will be duplicates for every one but doesn't seem to be an issue.
   [string[]]$OldDetections = (([regex]'(?<=SettingLogicalName=.)([^"]|\\")*').Matches($SDMPackageXML)).Value

   # Create your new detection clause(s) and remove all of the previous ones
   $MsiClause = New-CMDetectionClauseWindowsInstaller -ProductCode "<MSIGUID>" -ExpectedValue "<MSIVERSION>" -ExpressionOperator GreaterEquals -Value
   Set-CMScriptDeploymentType -ApplicationName "$($AppName)" -DeploymentTypeName "$($Title)" -AddDetectionClause ($MsiClause) -RemoveDetectionClause $OldDetections
}

This is a basic example of getting all current detection clauses of a deployment type and removing them with a newly created clause. (Getting an msi’s guid & version separately)