Toast-Notification-Script/New-ToastNotification.ps1
2020-08-22 21:06:10 +02:00

1956 lines
No EOL
106 KiB
PowerShell

<#
.SYNOPSIS
Create nice Windows 10 toast notifications for the logged on user in Windows 10.
.DESCRIPTION
Everything is customizeable through config-toast.xml.
Config-toast.xml can be locally or set to an UNC path with the -Config parameter.
This way you can quickly modify the configuration without the need to push new files to the computer running the toast.
Can be used for improving the numbers in Windows Servicing as well as kindly reminding users of pending reboots.
All actions are logged to a local log file in appdata\roaming\ToastNotification\New-Toastnotificaion.log.
.PARAMETER Config
Specify the path for the config.xml. If none is specified, the script uses the local config.xml
.NOTES
Filename: New-ToastNotification.ps1
Version: 2.0.0
Author: Martin Bengtsson
Blog: www.imab.dk
Twitter: @mwbengtsson
Version history:
1.0 - Script created
1.1 - Separated checks for pending reboot in registry/WMI from OS uptime.
More checks for conflicting options in config.xml.
The content of the config.xml is now imported with UTF-8 encoding enabling other characters to be used in the text boxes.
1.2 - Added option for personal greeting using given name retrieved from Active Directory. If no AD available, the script will use a placeholder.
Added ToastReboot protocol example, enabling the toast to carry out a potential reboot.
1.3 - All text elements in the toast notification is now customizeable through the config.xml
Expanded the options for finding given name. Now also looking in WMI if no local AD is available.
Added Get-WindowsVersion function: Testing for supported Windows version
Added Test-WindowsPushNotificationsEnabled function: Testing for OS toast blockers
Added some more detailed logging
Added contributions from @SuneThomsenDK @ https://www.osdsune.com
- Date formatting in deadline group
- Fixed a few script errors
- More text options
1.4 - Added new feature for checking for local active directory password expiration.
If the password is about to expire (days configured in config.xml), the toast notification will display reminding the users to change their password
1.4.1 - Get-ADPasswordExpiration function modified to not requiring the AD Powershell module. Thank you @ Andrew Wells :-)
Improved logging for when no toast notifications are displayed
More commenting
1.4.2 - Bug fixes to the date formatting of ADPasswordExpiration now correctly supporting different cultures
1.4.3 - Some minor corrections to the get-givenname function when retreiving first name from WMI and registry
Moved the default location for New-ToastNotification.log file to the user's profile
Added contribution from @kevmjohnston @ https://ccmcache.wordpress.com
- Added function for retrieving deadline date and time dynamically in WMI with ConfigMgr
1.5 - Added new option to run task sequences (PackageID) directly from the toast notification action button. Enable the option <RunPackageID> in the config.xml
Fixed a few script errors when running the script on a device without ConfigMgr client
1.6 - Added new option to run applications (ApplicationID) directly from the toast notification action button. Enable the option <RunApplicationID> in the config.xml
Created Display-ToastNotification function
- Displaying the toast notification as been trimmed and merged into its own function
Created Test-NTsystem function
- Testing if the script is being run as SYSTEM. This is not supported
Converted all Get-WMIObject to Get-CimInstance
- Get-WMIObject has been deprecated and is replaced with Get-CimInstance
1.7 - Added multilanguage support. Thank you Matt Benninge @matbe
- Script and config files now support multiple languages
- Note that old config xml files needs to be updated to support this
- Moved text values from option to the text-section for consistency
1.7.1 - Added 2 new options (LogoImageName and HeroImageName) to the config file, allowing switching of images more easily and dynamically
1.8.0 - Added support for using Windows 10 Toast Notification Script with Endpoint Analytics Proactive Remediation
- Added support for having config.xml file hosted online
- Added support for having images used in the script hosted online
** Most of the work done in version 2.0.0 is done by Chad Bower // @Brower_Cha on Twitter **
** I have added the additional protocols/scripts and rewritten some minor things **
** As well as added support for dynamic deadline retrieval for software updates **
** Stuff has been rewritten to suit my understanding and thoughts of the script **
2.0.0 - Huge changes to how this script handles custom protocols
- Added Support for Software Updates : Searches for an update (IPU) and will store in variable
- Added Support for Custom Actions/Protocols within the script under user context removing the need for that to be run under SYSTEM/ADMIN
<Option Name="Action" Value="ToastRunUpdateID:" />
- Added Support to dynamically create Custom Action Scripts to support Custom Protocols
- Added New XML Types for SoftwareUpdates :
<Option Name="RunUpdateTitle" Enabled="True" Value="Version 1909" />
<Option Name="RunUpdateID" Enabled="True" Value="3012973" />
- Added support for getting deadline date/time dynamically for software updates
.LINK
https://www.imab.dk/windows-10-toast-notification-script/
#>
[CmdletBinding()]
param(
[Parameter(HelpMessage='Path to XML Configuration File')]
[string]$Config
)
######### FUNCTIONS #########
# Create Write-Log function
function Write-Log() {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
[ValidateNotNullOrEmpty()]
[Alias("LogContent")]
[string]$Message,
# EDIT with your location for the local log file
[Parameter(Mandatory=$false)]
[Alias('LogPath')]
[string]$Path="$env:APPDATA\ToastNotificationScript\New-ToastNotification.log",
[Parameter(Mandatory=$false)]
[ValidateSet("Error","Warn","Info")]
[string]$Level="Info"
)
Begin {
# Set VerbosePreference to Continue so that verbose messages are displayed.
$VerbosePreference = 'Continue'
}
Process {
if ((Test-Path $Path)) {
$LogSize = (Get-Item -Path $Path).Length/1MB
$MaxLogSize = 5
}
# Check for file size of the log. If greater than 5MB, it will create a new one and delete the old.
if ((Test-Path $Path) -AND $LogSize -gt $MaxLogSize) {
Write-Error "Log file $Path already exists and file exceeds maximum file size. Deleting the log and starting fresh."
Remove-Item $Path -Force
$NewLogFile = New-Item $Path -Force -ItemType File
}
# If attempting to write to a log file in a folder/path that doesn't exist create the file including the path.
elseif (-NOT(Test-Path $Path)) {
Write-Verbose "Creating $Path."
$NewLogFile = New-Item $Path -Force -ItemType File
}
else {
# Nothing to see here yet.
}
# Format Date for our Log File
$FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
# Write message to error, warning, or verbose pipeline and specify $LevelText
switch ($Level) {
'Error' {
Write-Error $Message
$LevelText = 'ERROR:'
}
'Warn' {
Write-Warning $Message
$LevelText = 'WARNING:'
}
'Info' {
Write-Verbose $Message
$LevelText = 'INFO:'
}
}
# Write log entry to $Path
"$FormattedDate $LevelText $Message" | Out-File -FilePath $Path -Append
}
End {
}
}
# Create Pending Reboot function for registry
function Test-PendingRebootRegistry() {
Write-Log -Message "Running Test-PendingRebootRegistry function"
$CBSRebootKey = Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -ErrorAction Ignore
$WURebootKey = Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -ErrorAction Ignore
$FileRebootKey = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -ErrorAction Ignore
if (($CBSRebootKey -ne $null) -OR ($WURebootKey -ne $null) -OR ($FileRebootKey -ne $null)) {
Write-Log -Message "Check returned TRUE on ANY of the registry checks: Reboot is pending!"
return $true
}
Write-Log -Message "Check returned FALSE on ANY of the registry checks: Reboot is NOT pending!"
return $false
}
# Create Pending Reboot function for WMI via ConfigMgr client
function Test-PendingRebootWMI() {
Write-Log -Message "Running Test-PendingRebootWMI function"
if (Get-Service -Name ccmexec -ErrorAction SilentlyContinue) {
Write-Log -Message "Computer has ConfigMgr client installed - checking for pending reboots in WMI"
$Util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$Status = $Util.DetermineIfRebootPending()
if (($Status -ne $null) -AND ($Status.RebootPending)) {
Write-Log -Message "Check returned TRUE on checking WMI for pending reboot: Reboot is pending!"
return $true
}
Write-Log -Message "Check returned FALSE on checking WMI for pending reboot: Reboot is NOT pending!"
return $false
}
else {
Write-Log -Message "Computer has no ConfigMgr client installed - skipping checking WMI for pending reboots" -Level Warn
return $false
}
}
# Create Get Device Uptime function
function Get-DeviceUptime() {
Write-Log -Message "Running Get-DeviceUptime function"
$OS = Get-CimInstance Win32_OperatingSystem
$Uptime = (Get-Date) - ($OS.LastBootUpTime)
$Uptime.Days
}
# Create Get GivenName function
function Get-GivenName() {
Write-Log -Message "Running Get-GivenName function"
try {
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
$PrincipalContext = [System.DirectoryServices.AccountManagement.PrincipalContext]::new([System.DirectoryServices.AccountManagement.ContextType]::Domain, [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain())
$GivenName = ([System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($PrincipalContext,[System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName,[Environment]::UserName)).GivenName
$PrincipalContext.Dispose()
}
catch [System.Exception] {
Write-Log -Message "$_." -Level Warn
}
if ($GivenName) {
Write-Log -Message "Given name retrieved from Active Directory: $GivenName"
$GivenName
}
elseif (-NOT($GivenName)) {
Write-Log -Message "Given name not found in AD or no local AD available. Continuing looking for given name elsewhere"
if (Get-Service -Name ccmexec -ErrorAction SilentlyContinue) {
Write-Log -Message "Looking for given name in WMI with CCM client"
$LoggedOnSID = Get-CimInstance -Namespace ROOT\CCM -Class CCM_UserLogonEvents -Filter "LogoffTime=null" | Select -ExpandProperty UserSID
if ($LoggedOnSID.GetType().IsArray) {
Write-Log -Message "Multiple SID's found. Skipping"
$GivenName = ""
$GivenName
}
else {
$RegKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\SessionData"
$DisplayName = (Get-ChildItem -Path $RegKey | Where-Object {$_.GetValue("LoggedOnUserSID") -eq $LoggedOnSID} | Select-Object -First 1).GetValue("LoggedOnDisplayName")
if ($DisplayName) {
Write-Log -Message "Given name found in WMI with the CCM client: : $GivenName"
$GivenName = $DisplayName.Split()[0].Trim()
$GivenName
}
else {
$GivenName = ""
$GivenName
}
}
}
}
elseif (-NOT($GivenName)) {
# More options for given name here
}
else {
Write-Log -Message "No given name found. Using nothing as placeholder"
$GivenName = ""
$GivenName
}
}
# Create Get-WindowsVersion function
# This is used to determine if the script is running on Windows 10 or not
function Get-WindowsVersion() {
$OS = Get-CimInstance Win32_OperatingSystem
if (($OS.Version -like "10.0.*") -AND ($OS.ProductType -eq 1)) {
Write-Log -Message "Running supported version of Windows. Windows 10 and workstation OS detected"
$true
}
elseif ($OS.Version -notlike "10.0.*") {
Write-Log -Level Warn -Message "Not running supported version of Windows"
$false
}
else {
Write-Log -Level Warn -Message "Not running supported version of Windows"
$false
}
}
# Create Windows Push Notification function.
# This is testing if toast notifications generally are disabled within Windows 10
function Test-WindowsPushNotificationsEnabled() {
$ToastEnabledKey = (Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\PushNotifications" -Name ToastEnabled -ErrorAction Ignore).ToastEnabled
if ($ToastEnabledKey -eq "1") {
Write-Log -Message "Toast notifications are enabled in Windows"
$true
}
elseif ($ToastEnabledKey -eq "0") {
Write-Log -Level Warn -Message "Toast notifications are not enabled in Windows. The script will run, but toasts might not be displayed"
$false
}
else {
Write-Log -Message "The registry key for determining if toast notifications are enabled does not exist. The script will run, but toasts might not be displayed"
$false
}
}
# Create function for testing for local Active Directory password expiration.
# Thank you @ Andrew Wells :-)
function Get-ADPasswordExpiration([string]$fADPasswordExpirationDays) {
Write-Log -Message "Running Get-ADPasswordExpiration function"
try {
Write-Log -Message "Looking up SamAccountName and DomainName in local Active Directory"
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
$PrincipalContext = [System.DirectoryServices.AccountManagement.PrincipalContext]::new([System.DirectoryServices.AccountManagement.ContextType]::Domain,[System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain())
$SamAccountName = ([System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($PrincipalContext,[System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName,[Environment]::UserName)).SamAccountName
$DomainName = ([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()).Name
$PrincipalContext.Dispose()
}
catch [System.Exception] {
Write-Log -Message "$_." -Level Warn
}
if (($SamAccountName) -AND ($DomainName)) {
Write-Log -Message "SamAccountName found: $SamAccountName and DomainName found: $DomainName. Continuing looking for AD password expiration date"
try {
$Root = [ADSI] "LDAP://$($DomainName)"
$Searcher = New-Object System.DirectoryServices.DirectorySearcher($Root, "(SamAccountName=$($SamAccountName))")
$Searcher.PropertiesToLoad.Add("msDS-UserPasswordExpiryTimeComputed") | Out-Null
$Result = $Searcher.FindOne();
$ExpiryDate = [DateTime]::FromFileTime([Int64]::Parse((($Result.Properties["msDS-UserPasswordExpiryTimeComputed"])[0]).ToString()))
}
catch {
Write-Log -Message "Failed to retrieve password expiration date from Active Directory. Script is continuing, but without password expiration date" -Level Warn
}
if ($ExpiryDate) {
Write-Log -Message "Password expiration date found. Password is expiring on $ExpiryDate. Calculating time to expiration"
$LocalCulture = Get-Culture
$RegionDateFormat = [System.Globalization.CultureInfo]::GetCultureInfo($LocalCulture.LCID).DateTimeFormat.LongDatePattern
$ExpiryDate = Get-Date $ExpiryDate -f "$RegionDateFormat"
$Today = Get-Date -f "$RegionDateFormat"
$DateDiff = New-TimeSpan -Start $Today -End $ExpiryDate
if ($DateDiff.Days -le $fADPasswordExpirationDays -AND $DateDiff.Days -ge 0) {
Write-Log -Message "Password is expiring within the set period. Returning True"
Write-Log -Message "ADPasswordExpirationDays is set to: $fADPasswordExpirationDays"
# Return status, date and days until expiration
$true
$ExpiryDate
$DateDiff
}
else {
Write-Log -Message "Password is not expiring anytime soon. Returning False"
Write-Log -Message "ADPasswordExpirationDays is set to: $fADPasswordExpirationDays"
$false
}
}
elseif (-NOT($ExpiryDate)) {
Write-Log -Message "No password expiration date found. Returning False" -Level Warn
$false
}
}
elseif (-NOT($SamAccountName) -OR ($DomainName)) {
Write-Log -Message "Failed to retrieve SamAccountName or DomainName from local Active Directory. Script is continuing, but password expiration date cannot be retrieved" -Level Warn
$false
}
}
# Create function for retrieving deadline directly from WMI based on the PackageID.
# This works for Task Sequences, regular packages and software updates
# Thank you @kevmjohnston :-)
function Get-DynamicDeadline() {
Write-Log -Message "Running Get-DynamicDeadline function. Trying to get deadline details from WMI and ConfigMgr"
if (Get-Service -Name ccmexec -ErrorAction SilentlyContinue) {
if ($RunPackageIDEnabled -eq "True") {
Write-Log -Message "RunPackageIDEnabled is True. Trying to get deadline information based on package id"
try {
# Get task sequence program information from WMI. This is the same location used by Software Center
$PackageID = Get-CimInstance -Namespace root\ccm\clientsdk -Query "SELECT * FROM CCM_Program where PackageID = '$DynDeadlineValue'"
}
catch {
Write-Log -Message "Failed to get PackageID from WMI" -Level Warn
}
}
elseif ($RunUpdateIDEnabled -eq "True") {
Write-Log -Message "RunUpdateIDEnabled is True. Trying to get deadline information based on update id"
$UpdateID = Get-CMUpdate
}
else {
Write-Log -Level Warn "Currently no option enabled within the toast configuration which supports getting the deadline retrieved dynamically"
}
if (-NOT[string]::IsNullOrEmpty($PackageID)) {
# Get the deadline based on the package id
# The Where-Object clause filters out any old/dummy deadline values
# The Measure-Object clause returns only the earliest deadline if multiple program instances are found. In testing, I've only seen one instance
# per package ID even if multiple deployments of the same task sequence with different deadlines are targeted, so this is more of a failsafe
Write-Log -Message "PackageID retrieved. PackageID is: $DynDeadlineValue. Now getting deadline date and time"
$Deadline = ($PackageID | Where-Object {$_.Deadline -gt (Get-Date).AddDays(-1)} | Measure-Object -Property Deadline -Minimum).Minimum
if ($Deadline) {
# Deadline date and time retrieved. I'm formatting the date later on in the actual toast xml
Write-Log -Message "Deadline date and time successfully retrieved from WMI. Deadline is: $Deadline"
$Deadline.ToUniversalTime()
}
else {
Write-Log -Message "Failed to get deadline date and time from WMI" -Level Warn
Write-Log -Message "Please check if there really is a deadline configured" -Level Warn
Write-Log -Message "The script is continuing, but the toast is displayed without deadline date and time" -Level Warn
}
}
elseif (-NOT[string]::IsNullOrEmpty($UpdateID)) {
Write-Log -Message "UpdateID retrieved. UpdateID is: $DynDeadlineValue. Now getting deadline date and time"
if (-NOT[string]::IsNullOrEmpty($UpdateID.Deadline)) {
Write-Log -Message "Deadline date and time successfully retrieved from WMI. Deadline is: $($UpdateID.Deadline)"
$UpdateID.Deadline.ToUniversalTime()
}
else {
Write-Log -Message "Failed to get deadline date and time from WMI" -Level Warn
Write-Log -Message "Please check if there really is a deadline configured" -Level Warn
Write-Log -Message "The script is continuing, but the toast is displayed without deadline date and time" -Level Warn
}
}
else {
Write-Log -Message "Appears that the specified package ID or update ID: $DynDeadlineValue is not deployed to the device."
}
}
else {
Write-Log -Message "ConfigMgr service not found. This function requires the ConfigMgr client to be installed" -Level Warn
}
}
# Get-CMUpdate function
# This gets information about a deployed software update from WMI on the device
# HUGE shout-out to Chad Brower // @Brower_Cha on Twitter
# Added in version 2.0.0
function Get-CMUpdate() {
Write-Log -Message "Running Get-CMUpdate function"
# If the ConfigMgr service exist
if (Get-Service -Name ccmexec -ErrorAction SilentlyContinue) {
try {
# Get update information from WMI based on UpdateID and UpdateTitle
$GetCMUpdate = Get-CimInstance -Namespace root\ccm\clientSDK -Query SELECT * FROM CCM_SoftwareUpdate WHERE ArticleID = '$RunUpdateIDValue' AND Name LIKE '%$RunUpdateTitleValue%'
}
catch {
Write-Log -Level Error -Message "Failed to retrieve UpdateID from WMI with the CM client"
}
# If an update is based on the details from config.xml is deployed to the computer
if (-NOT[string]::IsNullOrEmpty($GetCMUpdate)) {
# Check EvaluationState: https://docs.microsoft.com/en-us/mem/configmgr/develop/reference/core/clients/sdk/ccm_softwareupdate-client-wmi-class
# There are 28 eval states. Perhaps add them all in the future
switch ($GetCMUpdate.EvaluationState) {
0 { $EvaluationState = 'None' }
1 { $EvaluationState = 'Available' }
2 { $EvaluationState = 'Submitted' }
7 { $EvaluationState = 'Installing' }
8 { $EvaluationStatet = 'Reboot' }
9 { $EvaluationState = 'Reboot' }
13 { $EvaluationState = 'Error' }
}
# If the evaluation of the update is in a desired state, write the details to output
if ($EvaluationState -eq "None" -OR $EvaluationState -eq "Available" -OR $EvaluationState -eq "Submitted") {
Write-Log -Level Info -Message "Found update that matches UpdateID: $($GetCMUpdate.ArticleID) and name: $($GetCMUpdate.Name)."
$GetCMUpdate
}
elseif ($EvaluationState -eq "Error") {
Write-Log -Message "UpdateID: $($GetCMUpdate.ArticleID) is in evaluation state: $EvaluationState. Retrying installation"
$GetCMUpdate
}
else {
Write-Log -Level Warn -Message "EvalutationState of UpdateID: $($GetCMUpdate.ArticleID) is not set to available. EvaluationState is: $EvaluationState."
Write-Log -Level Warn -Message "Script will exit here. Not displaying toast notification when when eval state is: $EvaluationState"
Exit 1
}
}
else {
Write-Log -Level Warn -Message "Specified update was not found on system. UpdateID: $RunUpdateIDValue and name: $RunUpdateTitleValue. Please check deployment in ConfigMgr"
Write-Log -Level Warn -Message "Script will exit here. Not displaying toast notification when specified update is not deployed"
Exit 1
}
}
else {
Write-Log -Level Warn -Message "ConfigMgr service not found. This function requires the ConfigMgr client to be installed"
}
}
# Create Write-PackageIDRegistry function
function Write-PackageIDRegistry() {
Write-Log -Message "Running Write-PackageIDRegistry function"
$RegistryPath = "HKCU:\SOFTWARE\ToastNotificationScript"
$RegistryName = "RunPackageID"
# Making sure that the registry path being used exists
if (-NOT(Test-Path -Path $RegistryPath)) {
try {
New-Item -Path $RegistryPath -Force
}
catch {
Write-Log -Message "Error. Could not create ToastNotificationScript registry path" -Level Warn
}
}
# If the PackageID specified in the config.xml is picked up
if ($RunPackageIDValue) {
# If the ConfigMgr service exist
if (Get-Service -Name ccmexec -ErrorAction SilentlyContinue) {
# Testing if the PackageID specified in the config.xml actually is deployed to the device
try {
$TestPackageID = Get-CimInstance -Namespace root\ccm\clientsdk -Query "SELECT * FROM CCM_Program WHERE PackageID = '$RunPackageIDValue'"
}
catch {
Write-Log -Level Warn -Message "Failed to retrieve $RunPackageIDValue from WMI"
}
# If the PackageID is found in WMI with the ConfigMgr client, tattoo that PackageID into registry
if ($TestPackageID) {
Write-Log -Message "PackageID: $RunPackageIDValue was found in WMI as deployed to the client"
Write-Log -Message "Writing the PackageID to registry"
if ((Get-ItemProperty -Path $RegistryPath -Name $RegistryName -ErrorAction SilentlyContinue).$RegistryName -ne $RunPackageIDValue) {
try {
New-ItemProperty -Path $RegistryPath -Name $RegistryName -Value $RunPackageIDValue -PropertyType "String" -Force
}
catch {
Write-Log -Level Warn -Message "Failed to write PackageID: $RunPackageIDValue to registry"
}
}
}
else {
Write-Log -Level Warn -Message "PackageID: $RunPackageIDValue was not found in WMI as deployed to the client. Please check the config.xml or deployment in ConfigMgr"
Write-Log -Level Warn -Message "Script will exit here. Not displaying toast notification when specified package is not deployed"
Exit 1
}
}
else {
Write-Log -Level Warn -Message "No ConfigMgr service found. This function requires the ConfigMgr client to be installed"
}
}
}
# Create Write-ApplicationIDRegistry function
function Write-ApplicationIDRegistry() {
Write-Log -Message "Running Write-ApplicationIDRegistry function"
$RegistryPath = "HKCU:\SOFTWARE\ToastNotificationScript"
$RegistryName = "RunApplicationID"
# Making sure that the registry path being used exists
if (-NOT(Test-Path -Path $RegistryPath)) {
try {
New-Item -Path $RegistryPath -Force
}
catch {
Write-Log -Level Warn -Message "Error. Could not create ToastNotificationScript registry path"
}
}
# If the ApplicationID specified in the config.xml is picked up
if ($RunApplicationIDValue) {
# If the ConfigMgr service exist
if (Get-Service -Name ccmexec -ErrorAction SilentlyContinue) {
# Testing if the ApplicationID specified in the config.xml actually is deployed to the device
try {
$TestApplicationID = Get-CimInstance -ClassName CCM_Application -Namespace "root\ccm\clientSDK" | Where-Object {$_.Id -eq $RunApplicationIDValue}
}
catch {
Write-Log -Level Warn -Message "Failed to retrieve $RunApplicationIDValue from WMI"
}
# If the ApplicationID is found in WMI with the ConfigMgr client, tattoo that ApplicationID into registry
if ($TestApplicationID) {
Write-Log -Message "ApplicationID: $RunApplicationIDValue was found in WMI as deployed to the client"
Write-Log -Message "Writing the ApplicationID to registry"
if ((Get-ItemProperty -Path $RegistryPath -Name $RegistryName -ErrorAction SilentlyContinue).$RegistryName -ne $RunApplicationIDValue) {
try {
New-ItemProperty -Path $RegistryPath -Name $RegistryName -Value $RunApplicationIDValue -PropertyType "String" -Force
}
catch {
Write-Log -Level Warn -Message "Failed to write ApplicationID: $RunApplicationIDValue to registry"
}
}
}
else {
Write-Log -Level Warn -Message "ApplicationID: $RunApplicationIDValue was not found in WMI as deployed to the client. Please check the config.xml or deployment in ConfigMgr"
Write-Log -Level Warn -Message "Script will exit here. Not displaying toast notification when specified application is not deployed"
Exit 1
}
}
else {
Write-Log -Level Warn -Message "No ConfigMgr service found. This function requires the ConfigMgr client to be installed"
}
}
}
# Create Write-UpdateIDRegistry
# This function writes the UpdateID to registry when used with Software (Feature) Updates in ConfigMgr
# HUGE shout-out to Chad Brower // @Brower_Cha on Twitter
# Added in version 2.0.0
function Write-UpdateIDRegistry() {
Write-Log -Message "Running Write-UpdateIDRegistry function"
$RegistryPath = "HKCU:\SOFTWARE\ToastNotificationScript"
$RegistryName = "RunUpdateID"
# Making sure that the registry path being used exists
if (-NOT(Test-Path -Path $RegistryPath)) {
try {
New-Item -Path $RegistryPath -Force
}
catch {
Write-Log -Level Warn -Message "Error. Could not create ToastNotificationScript registry path"
}
}
# If the UpdateID specified in the config.xml is picked up
if (-NOT[string]::IsNullOrEmpty($RunUpdateIDValue)) {
# If the ConfigMgr service exist
if (Get-Service -Name ccmexec -ErrorAction SilentlyContinue) {
# Getting the UpdateID specified in the config.xml via Get-CMUpdate function
try {
$GetUpdateID = Get-CMUpdate
}
catch {
Write-Log -Level Warn -Message "Failed to successfully run the Get-CMUpdate function"
}
# If the UpdateID is found in WMI with the ConfigMgr client, tattoo that UpdateID into registry
if (-NOT[string]::IsNullOrEmpty($GetUpdateID.UpdateID)) {
Write-Log -Message "Get-CMDUpdate was successfully run and UpdateID was retrieved"
Write-Log -Message "Writing the UpdateID to registry"
if ((Get-ItemProperty -Path $RegistryPath -Name $RegistryName -ErrorAction SilentlyContinue).$RegistryName -ne $GetUpdateID.UpdateID) {
try {
New-ItemProperty -Path $RegistryPath -Name $RegistryName -Value $GetUpdateID.UpdateID -PropertyType "String" -Force
}
catch {
Write-Log -Level Warn -Message "Failed to write UpdateID: $($GetUpdateID.UpdateID) to registry"
}
}
}
else {
Write-Log -Level Warn -Message "UpdateID: $RunUpdateIDValue was not found in WMI as deployed to the client. Please check the config.xml or deployment in ConfigMgr"
}
}
else {
Write-Log -Level Warn -Message "No ConfigMgr service found. This function requires the ConfigMgr client to be installed"
}
}
}
# Create Display-ToastNotification function
function Display-ToastNotification() {
$Load = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
$Load = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]
# Load the notification into the required format
$ToastXml = New-Object -TypeName Windows.Data.Xml.Dom.XmlDocument
$ToastXml.LoadXml($Toast.OuterXml)
# Display the toast notification
try {
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($App).Show($ToastXml)
Write-Log -Message "All good. Toast notification was displayed"
# Using Write-Output for sending status to IME log when used with Endpoint Analytics in Intune
Write-Output "All good. Toast notification was displayed"
Exit 0
}
catch {
Write-Log -Message "Something went wrong when displaying the toast notification" -Level Warn
Write-Log -Message "Make sure the script is running as the logged on user" -Level Warn
# Using Write-Output for sending status to IME log when used with Endpoint Analytics in Intune
Write-Output "Something went wrong when displaying the toast notification. Make sure the script is running as the logged on user"
Exit 1
}
if ($CustomAudio -eq "True") {
Invoke-Command -ScriptBlock {
Add-Type -AssemblyName System.Speech
$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer
$speak.Speak($CustomAudioTextToSpeech)
$speak.Dispose()
}
}
}
# Create Test-NTSystem function
# If the script is being run as SYSTEM, the toast notification won't display
function Test-NTSystem() {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
if ($currentUser.IsSystem -eq $true) {
Write-Log -Message "The script is being run as SYSTEM. This is not supported. The script needs the current user's context" -Level Warn
$true
}
elseif ($currentUser.IsSystem -eq $false) {
$false
}
}
# Write-CustomActionRegistry function
# This function creates custom protocols for the logged on user in HKCU.
# This will remove the need to create the protocols outside of the toast notification script
# HUGE shout-out to Chad Brower // @Brower_Cha on Twitter
# Added in version 2.0.0
function Write-CustomActionRegistry() {
[CmdletBinding()]
param (
[Parameter(Position="0")]
[ValidateSet("ToastRunApplicationID","ToastRunPackageID","ToastRunUpdateID","ToastReboot")]
[string]
$ActionType,
[Parameter(Position="1")]
[string]
$RegCommandPath = $CustomScriptPath
)
switch ($ActionType) {
ToastReboot {
# Build out registry for custom action
if((Test-Path -LiteralPath "HKCU:\Software\Classes\$($ActionType)\shell\open\command") -ne $true) {
try {
New-Item "HKCU:\Software\Classes\$($ActionType)\shell\open\command" -Force -ErrorAction SilentlyContinue | Out-Null
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)" -Name 'URL Protocol' -Value '' -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)" -Name '(default)' -Value "URL:$($ActionType) Protocol" -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
$RegCommandValue = $RegCommandPath + '\' + "$($ActionType).cmd"
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)\shell\open\command" -Name '(default)' -Value $RegCommandValue -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
}
catch {
Write-Log -Level Error "Failed to create the $ActionType custom protocol in HKCU\Software\Classes. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
}
}
ToastRunUpdateID {
# Build out registry for custom action
if((Test-Path -LiteralPath "HKCU:\Software\Classes\$($ActionType)\shell\open\command") -ne $true) {
try {
New-Item "HKCU:\Software\Classes\$($ActionType)\shell\open\command" -Force -ErrorAction SilentlyContinue | Out-Null
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)" -Name 'URL Protocol' -Value '' -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)" -Name '(default)' -Value "URL:$($ActionType) Protocol" -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
$RegCommandValue = $RegCommandPath + '\' + "$($ActionType).cmd"
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)\shell\open\command" -Name '(default)' -Value $RegCommandValue -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
}
catch {
Write-Log -Level Error "Failed to create the $ActionType custom protocol in HKCU\Software\Classes. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
}
}
ToastRunPackageID {
# Build out registry for custom action
if((Test-Path -LiteralPath "HKCU:\Software\Classes\$($ActionType)\shell\open\command") -ne $true) {
try {
New-Item "HKCU:\Software\Classes\$($ActionType)\shell\open\command" -Force -ErrorAction SilentlyContinue | Out-Null
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)" -Name 'URL Protocol' -Value '' -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)" -Name '(default)' -Value "URL:$($ActionType) Protocol" -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
$RegCommandValue = $RegCommandPath + '\' + "$($ActionType).cmd"
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)\shell\open\command" -Name '(default)' -Value $RegCommandValue -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
}
catch {
Write-Log -Level Error "Failed to create the $ActionType custom protocol in HKCU\Software\Classes. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
}
}
ToastRunApplicationID {
# Build out registry for custom action
if((Test-Path -LiteralPath "HKCU:\Software\Classes\$($ActionType)\shell\open\command") -ne $true) {
try {
New-Item "HKCU:\Software\Classes\$($ActionType)\shell\open\command" -Force -ErrorAction SilentlyContinue | Out-Null
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)" -Name 'URL Protocol' -Value '' -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)" -Name '(default)' -Value "URL:$($ActionType) Protocol" -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
$RegCommandValue = $RegCommandPath + '\' + "$($ActionType).cmd"
New-ItemProperty -LiteralPath "HKCU:\Software\Classes\$($ActionType)\shell\open\command" -Name '(default)' -Value $RegCommandValue -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
}
catch {
Write-Log -Level Error "Failed to create the $ActionType custom protocol in HKCU\Software\Classes. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
}
}
}
}
# Write-CustomActionScript function
# This function creates the custom scripts in ProgramData\ToastNotificationScript which is used to carry out custom protocol actions
# HUGE shout-out to Chad Brower // @Brower_Cha on Twitter
# Added in version 2.0.0
function Write-CustomActionScript() {
[CmdletBinding()]
param (
[Parameter(Position="0")]
[ValidateSet("ToastRunApplicationID","ToastRunPackageID","ToastRunUpdateID","ToastReboot")]
[string]
$Type,
[Parameter(Position="1")]
[String]
$Path = $CustomScriptPath # Global var can be changed
)
# Create Path for custom scipts to live
if (-NOT(Test-Path -Path $Path)) {
Write-Log -Message "CustomScriptPath not found. Creating it: $Path"
try {
New-item -Path $Path -ItemType Directory -Force | Out-Null
}
catch {
Write-Log -Level Error -Message "Failed to create the CustomScriptPath folder: $Path"
}
}
switch ($Type) {
ToastRunUpdateID {
try {
$CMDFileName = $Type + '.cmd'
$CMDFilePath = $CustomScriptPath + '\' + $CMDFileName
if (-NOT(Test-Path -Path $CMDFilePath)) {
Write-Log -Message "$CMDFileName does not exist - creating it"
try {
New-item -Path $CustomScriptPath -Name $CMDFileName -Force -OutVariable PathInfo | Out-Null
}
catch {
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
try {
$GetCustomScriptPath = $PathInfo.FullName
[String]$Script = 'powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File "C:\ProgramData\ToastNotificationScript\ToastRunUpdateID.ps1"'
if (-NOT[string]::IsNullOrEmpty($Script)) {
Out-File -FilePath $GetCustomScriptPath -InputObject $Script -Encoding ASCII -Force
}
}
catch {
Write-Log -Level Error "Failed to create the custom .cmd script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
}
else {
Write-Log -Message "$CMDFileName already exist. Doing nothing"
}
}
catch {
Write-Log -Level Error "Failed to create the custom .cmd script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
try {
$PS1FileName = $Type + '.ps1'
$PS1FilePath = $CustomScriptPath + '\' + $PS1FileName
if (-NOT(Test-Path -Path $PS1FilePath)) {
Write-Log -Message "$PS1FileName does not exist - creating it"
try {
New-item -Path $CustomScriptPath -Name $PS1FileName -Force -OutVariable PathInfo | Out-Null
}
catch {
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
try {
$GetCustomScriptPath = $PathInfo.FullName
[String]$Script = @'
$RegistryPath = "HKCU:\SOFTWARE\ToastNotificationScript"
$UpdateID = (Get-ItemProperty -Path $RegistryPath -Name "RunUpdateID").RunUpdateID
$Arguments = Get-WmiObject -Namespace root\ccm\clientSDK -Query "SELECT * FROM CCM_SoftwareUpdate WHERE UpdateID = '$UpdateID'"
Invoke-WmiMethod -Namespace root\ccm\clientsdk -Class CCM_SoftwareUpdatesManager -Name InstallUpdates -ArgumentList (,$Arguments)
if (Test-Path -Path "C:\Windows\CCM\ClientUX\SCClient.exe") { Start-Process -FilePath "C:\Windows\CCM\ClientUX\SCClient.exe" -ArgumentList "SoftwareCenter:Page=Updates" -WindowStyle Maximized }
exit 0
'@
if (-NOT[string]::IsNullOrEmpty($Script)) {
Out-File -FilePath $GetCustomScriptPath -InputObject $Script -Encoding ASCII -Force
}
}
catch {
Write-Log -Level Error "Failed to create the custom .ps1 script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
}
else {
Write-Log -Message "$PS1FileName already exist. Doing nothing"
}
}
catch {
Write-Log -Level Error "Failed to create the custom .ps1 script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
# Do not run another type; break
Break
}
ToastReboot {
try {
$CMDFileName = $Type + '.cmd'
$CMDFilePath = $CustomScriptPath + '\' + $CMDFileName
# Testing to see if the CMD file already exist.
# If it already exist and it was created by another user, permission issues would occur when trying to overwrite it
if (-NOT(Test-Path -Path $CMDFilePath)) {
Write-Log -Message "$CMDFileName does not exist - creating it"
try {
New-item -Path $CustomScriptPath -Name $CMDFileName -Force -OutVariable PathInfo | Out-Null
}
catch {
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
try {
$GetCustomScriptPath = $PathInfo.FullName
[String]$Script = 'shutdown /r /t 0 /d p:0:0 /c "Toast Notification Reboot"'
if (-NOT[string]::IsNullOrEmpty($Script)) {
Out-File -FilePath $GetCustomScriptPath -InputObject $Script -Encoding ASCII -Force
}
}
catch {
Write-Log -Level Error "Failed to create the custom .cmd script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
}
else {
Write-Log -Message "$CMDFileName already exist. Doing nothing"
}
}
catch {
Write-Log -Level Error "Failed to create the custom .cmd script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
# Do not run another type; break
Break
}
ToastRunPackageID {
try {
$CMDFileName = $Type + '.cmd'
$CMDFilePath = $CustomScriptPath + '\' + $CMDFileName
if (-NOT(Test-Path -Path $CMDFilePath)) {
Write-Log -Message "$CMDFileName does not exist - creating it"
try {
New-item -Path $CustomScriptPath -Name $CMDFileName -Force -OutVariable PathInfo | Out-Null
}
catch {
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
try {
$GetCustomScriptPath = $PathInfo.FullName
[String]$Script = 'powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File "C:\ProgramData\ToastNotificationScript\ToastRunPackageID.ps1"'
if (-NOT[string]::IsNullOrEmpty($Script)) {
Out-File -FilePath $GetCustomScriptPath -InputObject $Script -Encoding ASCII -Force
}
}
catch {
Write-Log -Level Error "Failed to create the custom .cmd script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
}
else {
Write-Log -Message "$CMDFileName already exist. Doing nothing"
}
}
catch {
Write-Log -Level Error "Failed to create the custom .cmd script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
try {
$PS1FileName = $Type + '.ps1'
$PS1FilePath = $CustomScriptPath + '\' + $PS1FileName
if (-NOT(Test-Path -Path $PS1FilePath)) {
Write-Log -Message "$PS1FileName does not exist - creating it"
try {
New-item -Path $CustomScriptPath -Name $PS1FileName -Force -OutVariable PathInfo | Out-Null
}
catch {
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
try {
$GetCustomScriptPath = $PathInfo.FullName
[String]$Script = @'
$RegistryPath = "HKCU:\SOFTWARE\ToastNotificationScript"
$PackageID = (Get-ItemProperty -Path $RegistryPath -Name "RunPackageID").RunPackageID
$SoftwareCenter = New-Object -ComObject "UIResource.UIResourceMgr"
$ProgramID = "*"
$SoftwareCenter.ExecuteProgram($ProgramID,$PackageID,$true)
if (Test-Path -Path "C:\Windows\CCM\ClientUX\SCClient.exe") { Start-Process -FilePath "C:\Windows\CCM\ClientUX\SCClient.exe" -ArgumentList "SoftwareCenter:Page=OSD" -WindowStyle Maximized }
exit 0
'@
if (-NOT[string]::IsNullOrEmpty($Script)) {
Out-File -FilePath $GetCustomScriptPath -InputObject $Script -Encoding ASCII -Force
}
}
catch {
Write-Log -Level Error "Failed to create the custom .ps1 script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
}
else {
Write-Log -Message "$PS1FileName already exist. Doing nothing"
}
}
catch {
Write-Log -Level Error "Failed to create the custom .ps1 script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
# Do not run another type; break
Break
}
ToastRunApplicationID {
try {
$CMDFileName = $Type + '.cmd'
$CMDFilePath = $CustomScriptPath + '\' + $CMDFileName
if (-NOT(Test-Path -Path $CMDFilePath)) {
Write-Log -Message "$CMDFileName does not exist - creating it"
try {
New-item -Path $CustomScriptPath -Name $CMDFileName -Force -OutVariable PathInfo | Out-Null
}
catch {
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
try {
$GetCustomScriptPath = $PathInfo.FullName
[String]$Script = 'powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File "C:\ProgramData\ToastNotificationScript\ToastRunApplicationID.ps1"'
if (-NOT[string]::IsNullOrEmpty($Script)) {
Out-File -FilePath $GetCustomScriptPath -InputObject $Script -Encoding ASCII -Force
}
}
catch {
Write-Log -Level Error "Failed to create the custom .cmd script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
}
else {
Write-Log -Message "$CMDFileName already exist. Doing nothing"
}
}
catch {
Write-Log -Level Error "Failed to create the custom .cmd script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
try {
$PS1FileName = $Type + '.ps1'
$PS1FilePath = $CustomScriptPath + '\' + $PS1FileName
if (-NOT(Test-Path -Path $PS1FilePath)) {
Write-Log -Message "$PS1FileName does not exist - creating it"
try {
New-item -Path $CustomScriptPath -Name $PS1FileName -Force -OutVariable PathInfo | Out-Null
}
catch {
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
try {
$GetCustomScriptPath = $PathInfo.FullName
[String]$Script = @'
function Trigger-AppInstallation() {
param(
[Parameter(Mandatory=$true)]
[String]$appID
)
Begin {
$application = (Get-CimInstance -ClassName CCM_Application -Namespace "root\ccm\clientSDK" | Where-Object {$_.Id -eq $appID})
$args = @{EnforcePreference = [UINT32] 0
Id = "$($application.Id)"
IsMachineTarget = $application.IsMachineTarget
IsRebootIfNeeded = $false
Priority = 'High'
Revision = "$($application.Revision)"}
}
Process {
if ($application.InstallState -eq "NotInstalled") {
try {
Invoke-CimMethod -Namespace "root\ccm\clientSDK" -ClassName CCM_Application -MethodName Install -Arguments $args
}
catch { }
}
elseif ($application.InstallState -eq "Installed") {
try {
Invoke-CimMethod -Namespace "root\ccm\clientSDK" -ClassName CCM_Application -MethodName Repair -Arguments $args
}
catch { }
}
}
End { }
}
$registryPath = "HKCU:\SOFTWARE\ToastNotificationScript"
$applicationID = (Get-ItemProperty -Path $RegistryPath -Name "RunApplicationID").RunApplicationID
Trigger-AppInstallation -appID $applicationID
if (Test-Path -Path "C:\Windows\CCM\ClientUX\SCClient.exe") { Start-Process -FilePath "C:\Windows\CCM\ClientUX\SCClient.exe" -ArgumentList "SoftwareCenter:Page=InstallationStatus" -WindowStyle Maximized };
exit 0
'@
if (-NOT[string]::IsNullOrEmpty($Script)) {
Out-File -FilePath $GetCustomScriptPath -InputObject $Script -Encoding ASCII -Force
}
}
catch {
Write-Log -Level Error "Failed to create the custom .ps1 script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
}
else {
Write-Log -Message "$PS1FileName already exist. Doing nothing"
}
}
catch {
Write-Log -Level Error "Failed to create the custom .ps1 script for $Type. Action button might not work"
$ErrorMessage = $_.Exception.Message
Write-Log -Level Error -Message "$($ErrorMessage)"
}
# Do not run another type; break
Break
}
}
}
######### GENERAL VARIABLES #########
# Getting executing directory
$global:ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
# Get running OS build
$RunningOS = Get-CimInstance -Class Win32_OperatingSystem | Select-Object BuildNumber
# Get user culture for multilanguage support
$userCulture = (Get-Culture).Name
# Setting the default culture to en-US. This will be the default language if MultiLanguageSupport is not enabled in the config
$defaultUserCulture = "en-US"
# Temporary location for images if images are hosted online on blob storage or similar
$LogoImageTemp = "$env:TEMP\ToastLogoImage.jpg"
$HeroImageTemp = "$env:TEMP\ToastHeroImage.jpg"
# Setting path to local images
$ImagesPath = "file:///$global:ScriptPath/Images"
# Getting custom action script location
$global:CustomScriptPath = "$env:ALLUSERSPROFILE\ToastNotificationScript"
# Testing for prerequisites
# Testing if script is being run as SYSTEM. This is not supported as the toast notification needs the current user's context
$isSystem = Test-NTSystem
if ($isSystem -eq $True) {
Write-Log -Message "Aborting script" -Level Warn
Exit 1
}
# Test if the script is being run on a supported version of Windows. Windows 10 AND workstation OS is required
$SupportedWindowsVersion = Get-WindowsVersion
if ($SupportedWindowsVersion -eq $False) {
Write-Log -Message "Aborting script" -Level Warn
Exit 1
}
# Testing for blockers of toast notifications in Windows
$WindowsPushNotificationsEnabled = Test-WindowsPushNotificationsEnabled
# If no config file is set as parameter, use the default.
# Default is executing directory. In this case, the config-toast.xml must exist in same directory as the New-ToastNotification.ps1 file
if (-NOT($Config)) {
Write-Log -Message "No config file set as parameter. Using local config file"
$Config = Join-Path ($global:ScriptPath) "config-toast.xml"
}
# Load config.xml
# Catering for when config.xml is hosted online on blob storage or similar
# Loading the config.xml file here is relevant for when used with Endpoint Analytics in Intune
if (($Config.StartsWith("https://")) -OR ($Config.StartsWith("http://"))) {
Write-Log -Message "Specified config file seems hosted [online]. Treating it accordingly"
try { $testOnlineConfig = Invoke-WebRequest -Uri $Config -UseBasicParsing } catch { <# nothing to see here. Used to make webrequest silent #> }
if ($testOnlineConfig.StatusDescription -eq "OK") {
try {
$webClient = New-Object System.Net.WebClient
$webClient.Encoding = [System.Text.Encoding]::UTF8
$Xml = [xml]$webClient.DownloadString($Config)
Write-Log -Message "Successfully loaded $Config"
}
catch {
$ErrorMessage = $_.Exception.Message
Write-Log -Message "Error, could not read $Config" -Level Warn
Write-Log -Message "Error message: $ErrorMessage" -Level Warn
# Using Write-Output for sending status to IME log when used with Endpoint Analytics in Intune
Write-Output "Error, could not read $Config. Error message: $ErrorMessage"
Exit 1
}
}
else {
Write-Log -Level Warn -Message "The provided URL to the config does not reply or does not come back OK"
# Using Write-Output for sending status to IME log when used with Endpoint Analytics in Intune
Write-Output "The provided URL to the config does not reply or does not come back OK"
Exit 1
}
}
# Catering for when config.xml is hosted locally or on fileshare
elseif (-NOT($Config.StartsWith("https://")) -OR (-NOT($Config.StartsWith("http://")))) {
Write-Log -Message "Specified config file seems hosted [locally or fileshare]. Treating it accordingly"
if (Test-Path -Path $Config) {
try {
$Xml = [xml](Get-Content -Path $Config -Encoding UTF8)
Write-Log -Message "Successfully loaded $Config"
}
catch {
$ErrorMessage = $_.Exception.Message
Write-Log -Message "Error, could not read $Config" -Level Warn
Write-Log -Message "Error message: $ErrorMessage" -Level Warn
Exit 1
}
}
else {
Write-Log -Level Warn -Message "No config file found on the specified location [locally or fileshare]"
Exit 1
}
}
else {
Write-Log -Level Warn -Message "Something about the config file is completely off"
# Using Write-Output for sending status to IME log when used with Endpoint Analytics in Intune
Write-Output "Something about the config file is completely off"
Exit 1
}
# Load xml content into variables
if(-NOT[string]::IsNullOrEmpty($Xml)) {
try {
Write-Log -Message "Loading xml content from $Config into variables"
# Load Toast Notification features
$ToastEnabled = $Xml.Configuration.Feature | Where-Object {$_.Name -like 'Toast'} | Select-Object -ExpandProperty 'Enabled'
$UpgradeOS = $Xml.Configuration.Feature | Where-Object {$_.Name -like 'UpgradeOS'} | Select-Object -ExpandProperty 'Enabled'
$PendingRebootUptime = $Xml.Configuration.Feature | Where-Object {$_.Name -like 'PendingRebootUptime'} | Select-Object -ExpandProperty 'Enabled'
$PendingRebootCheck = $Xml.Configuration.Feature | Where-Object {$_.Name -like 'PendingRebootCheck'} | Select-Object -ExpandProperty 'Enabled'
$ADPasswordExpiration = $Xml.Configuration.Feature | Where-Object {$_.Name -like 'ADPasswordExpiration'} | Select-Object -ExpandProperty 'Enabled'
# Load Toast Notification options
$PendingRebootUptimeTextEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'PendingRebootUptimeText'} | Select-Object -ExpandProperty 'Enabled'
$MaxUptimeDays = $Xml.Configuration.Option | Where-Object {$_.Name -like 'MaxUptimeDays'} | Select-Object -ExpandProperty 'Value'
$PendingRebootCheckTextEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'PendingRebootCheckText'} | Select-Object -ExpandProperty 'Enabled'
$ADPasswordExpirationTextEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'ADPasswordExpirationText'} | Select-Object -ExpandProperty 'Enabled'
$ADPasswordExpirationDays = $Xml.Configuration.Option | Where-Object {$_.Name -like 'ADPasswordExpirationDays'} | Select-Object -ExpandProperty 'Value'
$TargetOS = $Xml.Configuration.Option | Where-Object {$_.Name -like 'TargetOS'} | Select-Object -ExpandProperty 'Build'
$DeadlineEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'Deadline'} | Select-Object -ExpandProperty 'Enabled'
$DeadlineContent = $Xml.Configuration.Option | Where-Object {$_.Name -like 'Deadline'} | Select-Object -ExpandProperty 'Value'
$DynDeadlineEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'DynamicDeadline'} | Select-Object -ExpandProperty 'Enabled'
$DynDeadlineValue = $Xml.Configuration.Option | Where-Object {$_.Name -like 'DynamicDeadline'} | Select-Object -ExpandProperty 'Value'
# Creating Scripts and Protocols
# Added in version 2.0.0
$CreateScriptsProtocolsEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'CreateScriptsAndProtocols'} | Select-Object -ExpandProperty 'Enabled'
$RunPackageIDEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'RunPackageID'} | Select-Object -ExpandProperty 'Enabled'
$RunPackageIDValue = $Xml.Configuration.Option | Where-Object {$_.Name -like 'RunPackageID'} | Select-Object -ExpandProperty 'Value'
$RunApplicationIDEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'RunApplicationID'} | Select-Object -ExpandProperty 'Enabled'
$RunApplicationIDValue = $Xml.Configuration.Option | Where-Object {$_.Name -like 'RunApplicationID'} | Select-Object -ExpandProperty 'Value'
# ConfigMgr Software Updates
# Added in version 2.0.0
$RunUpdateIDEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'RunUpdateID'} | Select-Object -ExpandProperty 'Enabled'
$RunUpdateIDValue = $Xml.Configuration.Option | Where-Object {$_.Name -like 'RunUpdateID'}| Select-Object -ExpandProperty 'Value'
$RunUpdateTitleEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'RunUpdateTitle'} | Select-Object -ExpandProperty 'Enabled'
$RunUpdateTitleValue = $Xml.Configuration.Option | Where-Object {$_.Name -like 'RunUpdateTitle'} | Select-Object -ExpandProperty 'Value'
$SCAppName = $Xml.Configuration.Option | Where-Object {$_.Name -like 'UseSoftwareCenterApp'} | Select-Object -ExpandProperty 'Name'
$SCAppStatus = $Xml.Configuration.Option | Where-Object {$_.Name -like 'UseSoftwareCenterApp'} | Select-Object -ExpandProperty 'Enabled'
$PSAppName = $Xml.Configuration.Option | Where-Object {$_.Name -like 'UsePowershellApp'} | Select-Object -ExpandProperty 'Name'
$PSAppStatus = $Xml.Configuration.Option | Where-Object {$_.Name -like 'UsePowershellApp'} | Select-Object -ExpandProperty 'Enabled'
$CustomAudio = $Xml.Configuration.Option | Where-Object {$_.Name -like 'CustomAudio'} | Select-Object -ExpandProperty 'Enabled'
$LogoImageFileName = $Xml.Configuration.Option | Where-Object {$_.Name -like 'LogoImageName'} | Select-Object -ExpandProperty 'Value'
$HeroImageFileName = $Xml.Configuration.Option | Where-Object {$_.Name -like 'HeroImageName'} | Select-Object -ExpandProperty 'Value'
# Rewriting image variables to cater for images being hosted online, as well as being hosted locally.
# Needed image including path in one variable
if ((-NOT[string]::IsNullOrEmpty($LogoImageFileName)) -OR (-NOT[string]::IsNullOrEmpty($HeroImageFileName))) {
$LogoImage = $ImagesPath + "/" + $LogoImageFileName
$HeroImage = $ImagesPath + "/" + $HeroImageFileName
}
$Scenario = $Xml.Configuration.Option | Where-Object {$_.Name -like 'Scenario'} | Select-Object -ExpandProperty 'Type'
$Action = $Xml.Configuration.Option | Where-Object {$_.Name -like 'Action'} | Select-Object -ExpandProperty 'Value'
$GreetGivenName = $Xml.Configuration.Text | Where-Object {$_.Option -like 'GreetGivenName'} | Select-Object -ExpandProperty 'Enabled'
$MultiLanguageSupport = $Xml.Configuration.Text | Where-Object {$_.Option -like 'MultiLanguageSupport'} | Select-Object -ExpandProperty 'Enabled'
# Load Toast Notification buttons
$ActionButtonEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'ActionButton'} | Select-Object -ExpandProperty 'Enabled'
$DismissButtonEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'DismissButton'} | Select-Object -ExpandProperty 'Enabled'
$SnoozeButtonEnabled = $Xml.Configuration.Option | Where-Object {$_.Name -like 'SnoozeButton'} | Select-Object -ExpandProperty 'Enabled'
# Multi language support
if ($MultiLanguageSupport -eq "True") {
Write-Log -Message "MultiLanguageSupport set to True. Current language culture is $userCulture. Checking for language support"
# Check config xml if language support is added for the users culture
if (-NOT[string]::IsNullOrEmpty($xml.Configuration.$userCulture)) {
Write-Log -Message "Support for the users language culture found, localizing text using $userCulture"
$XmlLang = $xml.Configuration.$userCulture
}
# Else fallback to using default language "en-US"
elseif (-NOT[string]::IsNullOrEmpty($xml.Configuration.$defaultUserCulture)) {
Write-Log -Message "No support for the users language culture found, using $defaultUserCulture as default fallback language"
$XmlLang = $xml.Configuration.$defaultUserCulture
}
}
# If multilanguagesupport is set to False use default language "en-US"
elseif ($MultiLanguageSupport -eq "False") {
$XmlLang = $xml.Configuration.$defaultUserCulture
}
# Regardless of whatever might happen, always use "en-US" as language
else {
$XmlLang = $xml.Configuration.$defaultUserCulture
}
# Load Toast Notification text
$PendingRebootUptimeTextValue = $XmlLang.Text | Where-Object {$_.Name -like 'PendingRebootUptimeText'} | Select-Object -ExpandProperty '#text'
$PendingRebootCheckTextValue = $XmlLang.Text | Where-Object {$_.Name -like 'PendingRebootCheckText'} | Select-Object -ExpandProperty '#text'
$ADPasswordExpirationTextValue = $XmlLang.Text | Where-Object {$_.Name -like 'ADPasswordExpirationText'} | Select-Object -ExpandProperty '#text'
$CustomAudioTextToSpeech = $XmlLang.Text | Where-Object {$_.Name -like 'CustomAudio'} | Select-Object -ExpandProperty '#text'
$ActionButtonContent = $XmlLang.Text | Where-Object {$_.Name -like 'ActionButton'} | Select-Object -ExpandProperty '#text'
$DismissButtonContent = $XmlLang.Text | Where-Object {$_.Name -like 'DismissButton'} | Select-Object -ExpandProperty '#text'
$SnoozeButtonContent = $XmlLang.Text | Where-Object {$_.Name -like 'SnoozeButton'} | Select-Object -ExpandProperty '#text'
$AttributionText = $XmlLang.Text | Where-Object {$_.Name -like 'AttributionText'} | Select-Object -ExpandProperty '#text'
$HeaderText = $XmlLang.Text | Where-Object {$_.Name -like 'HeaderText'} | Select-Object -ExpandProperty '#text'
$TitleText = $XmlLang.Text | Where-Object {$_.Name -like 'TitleText'} | Select-Object -ExpandProperty '#text'
$BodyText1 = $XmlLang.Text | Where-Object {$_.Name -like 'BodyText1'} | Select-Object -ExpandProperty '#text'
$BodyText2 = $XmlLang.Text | Where-Object {$_.Name -like 'BodyText2'} | Select-Object -ExpandProperty '#text'
$SnoozeText = $XmlLang.Text | Where-Object {$_.Name -like 'SnoozeText'} | Select-Object -ExpandProperty '#text'
$DeadlineText = $XmlLang.Text | Where-Object {$_.Name -like 'DeadlineText'} | Select-Object -ExpandProperty '#text'
$GreetMorningText = $XmlLang.Text | Where-Object {$_.Name -like 'GreetMorningText'} | Select-Object -ExpandProperty '#text'
$GreetAfternoonText = $XmlLang.Text | Where-Object {$_.Name -like 'GreetAfternoonText'} | Select-Object -ExpandProperty '#text'
$GreetEveningText = $XmlLang.Text | Where-Object {$_.Name -like 'GreetEveningText'} | Select-Object -ExpandProperty '#text'
$MinutesText = $XmlLang.Text | Where-Object {$_.Name -like 'MinutesText'} | Select-Object -ExpandProperty '#text'
$HourText = $XmlLang.Text | Where-Object {$_.Name -like 'HourText'} | Select-Object -ExpandProperty '#text'
$HoursText = $XmlLang.Text | Where-Object {$_.Name -like 'HoursText'} | Select-Object -ExpandProperty '#text'
$ComputerUptimeText = $XmlLang.Text | Where-Object {$_.Name -like 'ComputerUptimeText'} | Select-Object -ExpandProperty '#text'
$ComputerUptimeDaysText = $XmlLang.Text | Where-Object {$_.Name -like 'ComputerUptimeDaysText'} | Select-Object -ExpandProperty '#text'
Write-Log -Message "Successfully loaded xml content from $Config"
}
catch {
Write-Log -Message "Xml content from $Config was not loaded properly"
Exit 1
}
}
# Check if toast is enabled in config.xml
if ($ToastEnabled -ne "True") {
Write-Log -Message "Toast notification is not enabled. Please check $Config file"
Exit 1
}
# Checking for conflicts in config. Some combinations makes no sense, thus trying to prevent those from happening
if (($UpgradeOS -eq "True") -AND ($PendingRebootCheck -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have both ÜpgradeOS feature set to True AND PendingRebootCheck feature set to True at the same time"
Exit 1
}
if (($UpgradeOS -eq "True") -AND ($PendingRebootUptime -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have both ÜpgradeOS feature set to True AND PendingRebootUptime feature set to True at the same time"
Exit 1
}
if (($PendingRebootCheck -eq "True") -AND ($PendingRebootUptime -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You currently can't have both PendingReboot features set to True. Please use them seperately"
Exit 1
}
if (($ADPasswordExpiration -eq "True") -AND ($UpgradeOS -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have both ADPasswordExpiration AND UpgradeOS set to True at the same time"
Exit 1
}
if (($ADPasswordExpiration -eq "True") -AND ($PendingRebootCheck -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have both ADPasswordExpiration AND PendingRebootCheck set to True at the same time"
Exit 1
}
if (($ADPasswordExpiration -eq "True") -AND ($PendingRebootUptime -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have both ADPasswordExpiration AND PendingRebootUptime set to True at the same time"
Exit 1
}
if (($SCAppStatus -eq "True") -AND (-NOT(Get-Service -Name ccmexec))) {
Write-Log -Level Warn -Message "Error. Using Software Center app for the notification requires the ConfigMgr client installed"
Write-Log -Level Warn -Message "Error. Please install the ConfigMgr cient or use Powershell as app doing the notification"
Exit 1
}
if (($SCAppStatus -eq "True") -AND ($PSAppStatus -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have both SoftwareCenter app set to True AND PowershellApp set to True at the same time"
Exit 1
}
if (($SCAppStatus -ne "True") -AND ($PSAppStatus -ne "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You need to enable at least 1 app in the config doing the notification. ie. Software Center or Powershell"
Exit 1
}
if (($UpgradeOS -eq "True") -AND ($PendingRebootUptimeTextEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have UpgradeOS set to True and PendingRebootUptimeText set to True at the same time"
Exit 1
}
if (($UpgradeOS -eq "True") -AND ($PendingRebootCheckTextEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have UpgradeOS set to True and PendingRebootCheckText set to True at the same time"
Exit 1
}
if (($PendingRebootUptimeTextEnabled -eq "True") -AND ($PendingRebootCheckTextEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have PendingRebootUptimeText set to True and PendingRebootCheckText set to True at the same time"
Write-Log -Level Warn -Message "You should only enable one of the text options"
Exit 1
}
if (($PendingRebootCheck -eq "True") -AND ($PendingRebootUptimeTextEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have PendingRebootCheck set to True and PendingRebootUptimeText set to True at the same time"
Write-Log -Level Warn -Message "You should use PendingRebootCheck with the PendingRebootCheckText option instead"
Exit 1
}
if (($PendingRebootUptime -eq "True") -AND ($PendingRebootCheckTextEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have PendingRebootUptime set to True and PendingRebootCheckText set to True at the same time"
Write-Log -Level Warn -Message "You should use PendingRebootUptime with the PendingRebootUptimeText option instead"
Exit 1
}
if (($ADPasswordExpirationTextEnabled -eq "True") -AND ($PendingRebootCheckTextEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have ADPasswordExpirationTextEnabled set to True and PendingRebootCheckText set to True at the same time"
Write-Log -Level Warn -Message "You should only enable one of the text options"
Exit 1
}
if (($ADPasswordExpirationTextEnabled -eq "True") -AND ($PendingRebootUptimeTextEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have ADPasswordExpirationTextEnabled set to True and PendingRebootUptimeTextEnabled set to True at the same time"
Write-Log -Level Warn -Message "You should only enable one of the text options"
Exit 1
}
if (($DeadlineEnabled -eq "True") -AND ($DynDeadlineEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have DeadlineEnabled set to True and DynamicDeadlineEnabled set to True at the same time"
Write-Log -Level Warn -Message "You should only enable one of the deadline options"
Exit 1
}
if (($RunApplicationIDEnabled -eq "True") -AND ($RunPackageIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have RunApplicationIDEnabled set to True and RunPackageIDEnabled set to True at the same time"
Write-Log -Level Warn -Message "You should only enable one of the options"
Exit 1
}
if (($RunApplicationIDEnabled -eq "True") -AND ($RunUpdateIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have RunApplicationIDEnabled set to True and RunUpdateIDEnabled set to True at the same time"
Write-Log -Level Warn -Message "You should only enable one of the options"
Exit 1
}
if (($RunUpdateIDEnabled -eq "True") -AND ($RunPackageIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "Error. You can't have RunUpdateIDEnabled set to True and RunPackageIDEnabled set to True at the same time"
Write-Log -Level Warn -Message "You should only enable one of the options"
Exit 1
}
# New checks for conflicting selections. Trying to prevent that one option is enabled with the wrong action
# Example: Having RunUpdatesID enabled and expecting the toast action button to trigger installation of an update, but instead reboots the computer
# Added in version 2.0.0
if (($Action -eq "ToastRunApplicationID:") -AND ($RunUpdateIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "You are using the toast notification with RunUpdateIDEnabled set to $RunUpdateIDEnabled, but the action button is set to $Action"
Write-Log -Level Warn -Message "This seems like an unintended configuration"
Exit 1
}
if (($Action -eq "ToastRunPackageID:") -AND ($RunUpdateIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "You are using the toast notification with RunUpdateIDEnabled set to $RunUpdateIDEnabled, but the action button is set to $Action"
Write-Log -Level Warn -Message "This seems like an unintended configuration"
Exit 1
}
if (($Action -eq "ToastReboot:") -AND ($RunUpdateIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "You are using the toast notification with RunUpdateIDEnabled set to $RunUpdateIDEnabled, but the action button is set to $Action"
Write-Log -Level Warn -Message "This seems like an unintended configuration"
Exit 1
}
if (($Action -eq "ToastRunApplicationID:") -AND ($RunPackageIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "You are using the toast notification with RunPackageIDEnabled set to $RunPackageIDEnabled, but the action button is set to $Action"
Write-Log -Level Warn -Message "This seems like an unintended configuration"
Exit 1
}
if (($Action -eq "ToastRunUpdateID:") -AND ($RunPackageIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "You are using the toast notification with RunPackageIDEnabled set to $RunPackageIDEnabled, but the action button is set to $Action"
Write-Log -Level Warn -Message "This seems like an unintended configuration"
Exit 1
}
if (($Action -eq "ToastReboot:") -AND ($RunPackageIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "You are using the toast notification with RunPackageIDEnabled set to $RunPackageIDEnabled, but the action button is set to $Action"
Write-Log -Level Warn -Message "This seems like an unintended configuration"
Exit 1
}
if (($Action -eq "ToastRunPackageID:") -AND ($RunApplicationIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "You are using the toast notification with RunApplicationIDEnabled set to $RunApplicationIDEnabled, but the action button is set to $Action"
Write-Log -Level Warn -Message "This seems like an unintended configuration"
Exit 1
}
if (($Action -eq "ToastRunUpdateID:") -AND ($RunApplicationIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "You are using the toast notification with RunApplicationIDEnabled set to $RunApplicationIDEnabled, but the action button is set to $Action"
Write-Log -Level Warn -Message "This seems like an unintended configuration"
Exit 1
}
if (($Action -eq "ToastReboot:") -AND ($RunApplicationIDEnabled -eq "True")) {
Write-Log -Level Warn -Message "Error. Conflicting selection in the $Config file"
Write-Log -Level Warn -Message "You are using the toast notification with RunApplicationIDEnabled set to $RunApplicationIDEnabled, but the action button is set to $Action"
Write-Log -Level Warn -Message "This seems like an unintended configuration"
Exit 1
}
# Downloading images into user's temp folder if images are hosted online
if (($LogoImageFileName.StartsWith("https://")) -OR ($LogoImageFileName.StartsWith("http://"))) {
Write-Log -Message "ToastLogoImage appears to be hosted online. Will need to download the file"
# Testing to see if image at the provided URL indeed is available
try { $testOnlineLogoImage = Invoke-WebRequest -Uri $LogoImageFileName -UseBasicParsing } catch { <# nothing to see here. Used to make webrequest silent #> }
if ($testOnlineLogoImage.StatusDescription -eq "OK") {
try {
Invoke-WebRequest -Uri $LogoImageFileName -OutFile $LogoImageTemp
# Replacing image variable with the image downloaded locally
$LogoImage = $LogoImageTemp
Write-Log -Message "Successfully downloaded $LogoImageTemp from $LogoImageFileName"
}
catch {
Write-Log -Level Warn -Message "Failed to download the $LogoImageTemp from $LogoImageFileName"
}
}
else {
Write-Log -Level Warn -Message "The image supposedly located on $LogoImageFileName is not available"
}
}
if (($HeroImageFileName.StartsWith("https://")) -OR ($HeroImageFileName.StartsWith("http://"))) {
Write-Log -Message "ToastHeroImage appears to be hosted online. Will need to download the file"
# Testing to see if image at the provided URL indeed is available
try { $testOnlineHeroImage = Invoke-WebRequest -Uri $HeroImageFileName -UseBasicParsing } catch { <# nothing to see here. Used to make webrequest silent #> }
if ($testOnlineHeroImage.StatusDescription -eq "OK") {
try {
Invoke-WebRequest -Uri $HeroImageFileName -OutFile $HeroImageTemp
# Replacing image variable with the image downloaded locally
$HeroImage = $HeroImageTemp
Write-Log -Message "Successfully downloaded $HeroImageTemp from $HeroImageFileName"
}
catch {
Write-Log -Level Warn -Message "Failed to download the $HeroImageTemp from $HeroImageFileName"
}
}
else {
Write-Log -Level Warn -Message "The image supposedly located on $HeroImageFileName is not available"
}
}
# Creating custom scripts and protocols if enabled in the config
if ($CreateScriptsProtocolsEnabled -eq "True") {
Write-Log -Message "CreateScriptsAndProtocols set to True. Will create custom scripts and protocols automatically for the logged on user"
Write-CustomActionRegistry -ActionType ToastReboot
Write-CustomActionRegistry -ActionType ToastRunApplicationID
Write-CustomActionRegistry -ActionType ToastRunPackageID
Write-CustomActionRegistry -ActionType ToastRunUpdateID
Write-CustomActionScript -Type ToastReboot
Write-CustomActionScript -Type ToastRunApplicationID
Write-CustomActionScript -Type ToastRunPackageID
Write-CustomActionScript -Type ToastRunUpdateID
}
# Running RunUpdateID function
if ($RunUpdateIDEnabled -eq "True") {
Write-Log -Message "RunUpdateID set to True. Will allow execution of Software UpdateID (KB-article number) directly from the toast action button"
Write-UpdateIDRegistry
}
# Running RunApplicationID function
if ($RunApplicationIDEnabled -eq "True") {
Write-Log -Message "RunApplicationID set to True. Will allow execution of ApplicationID directly from the toast action button"
Write-ApplicationIDRegistry
}
# Running RunPackageID function
if ($RunPackageIDEnabled -eq "True") {
Write-Log -Message "RunPackageID set to True. Will allow execution of PackageID directly from the toast action button"
Write-PackageIDRegistry
}
# Running DynamicDeadline function
if ($DynDeadlineEnabled -eq "True") {
Write-Log -Message "DynDeadlineEnabled set to True. Overriding deadline details using date and time from WMI"
$DeadlineContent = Get-DynamicDeadline
}
# Running ADPasswordExpiration Check
if ($ADPasswordExpiration -eq "True") {
Write-Log -Message "ADPasswordExpiration set to True. Checking for expiring AD password"
$TestADPasswordExpiration = Get-ADPasswordExpiration $ADPasswordExpirationDays
$ADPasswordExpirationResult = $TestADPasswordExpiration[0]
$ADPasswordExpirationDate = $TestADPasswordExpiration[1]
$ADPasswordExpirationDiff = $TestADPasswordExpiration[2]
}
# Running Pending Reboot Checks
if ($PendingRebootCheck -eq "True") {
Write-Log -Message "PendingRebootCheck set to True. Checking for pending reboots"
$TestPendingRebootRegistry = Test-PendingRebootRegistry
$TestPendingRebootWMI = Test-PendingRebootWMI
}
if ($PendingRebootUptime -eq "True") {
$Uptime = Get-DeviceUptime
Write-Log -Message "PendingRebootUptime set to True. Checking for device uptime. Current uptime is: $Uptime days"
}
# Check for required entries in registry for when using Software Center as application for the toast
if ($SCAppStatus -eq "True") {
if (Get-Service -Name ccmexec -ErrorAction SilentlyContinue) {
# Path to the notification app doing the actual toast
$RegPath = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings"
$App = "Microsoft.SoftwareCenter.DesktopToasts"
# Creating registry entries if they don't exists
if (-NOT(Test-Path -Path $RegPath\$App)) {
New-Item -Path $RegPath\$App -Force
New-ItemProperty -Path $RegPath\$App -Name "ShowInActionCenter" -Value 1 -PropertyType "DWORD" -Force
New-ItemProperty -Path $RegPath\$App -Name "Enabled" -Value 1 -PropertyType "DWORD" -Force
}
# Make sure the app used with the action center is enabled
if ((Get-ItemProperty -Path $RegPath\$App -Name "Enabled" -ErrorAction SilentlyContinue).Enabled -ne "1") {
New-ItemProperty -Path $RegPath\$App -Name "Enabled" -Value 1 -PropertyType "DWORD" -Force
}
}
else {
Write-Log -Message "No ConfigMgr client thus cannot use Software Center as notifying app" -Level Warn
}
}
# Check for required entries in registry for when using Powershell as application for the toast
if ($PSAppStatus -eq "True") {
# Register the AppID in the registry for use with the Action Center, if required
$RegPath = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings"
$App = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe"
# Creating registry entries if they don't exists
if (-NOT(Test-Path -Path $RegPath\$App)) {
New-Item -Path $RegPath\$App -Force
New-ItemProperty -Path $RegPath\$App -Name "ShowInActionCenter" -Value 1 -PropertyType "DWORD"
}
# Make sure the app used with the action center is enabled
if ((Get-ItemProperty -Path $RegPath\$App -Name "ShowInActionCenter" -ErrorAction SilentlyContinue).ShowInActionCenter -ne "1") {
New-ItemProperty -Path $RegPath\$App -Name "ShowInActionCenter" -Value 1 -PropertyType "DWORD" -Force
}
}
# Checking if running toast with personal greeting with given name
if ($GreetGivenName -eq "True") {
Write-Log -Message "Greeting with given name selected. Replacing HeaderText"
$Hour = (Get-Date).TimeOfDay.Hours
if (($Hour -ge 0) -AND ($Hour -lt 12)) {
Write-Log -Message "Greeting with $GreetMorningText"
$Greeting = $GreetMorningText
}
elseif (($Hour -ge 12) -AND ($Hour -lt 16)) {
Write-Log -Message "Greeting with $GreetAfternoonText"
$Greeting = $GreetAfternoonText
}
else {
Write-Log -Message "Greeting with personal greeting: $GreetEveningText"
$Greeting = $GreetEveningText
}
$GivenName = Get-GivenName
$HeaderText = "$Greeting $GivenName"
}
# Formatting the toast notification XML
# Create the default toast notification XML with action button and dismiss button
if (($ActionButtonEnabled -eq "True") -AND ($DismissButtonEnabled -eq "True")) {
Write-Log -Message "Creating the xml for displaying both action button and dismiss button"
[xml]$Toast = @"
<toast scenario="$Scenario">
<visual>
<binding template="ToastGeneric">
<image placement="hero" src="$HeroImage"/>
<image id="1" placement="appLogoOverride" hint-crop="circle" src="$LogoImage"/>
<text placement="attribution">$AttributionText</text>
<text>$HeaderText</text>
<group>
<subgroup>
<text hint-style="title" hint-wrap="true" >$TitleText</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$BodyText1</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$BodyText2</text>
</subgroup>
</group>
</binding>
</visual>
<actions>
<action activationType="protocol" arguments="$Action" content="$ActionButtonContent" />
<action activationType="system" arguments="dismiss" content="$DismissButtonContent"/>
</actions>
</toast>
"@
}
# NO action button and NO dismiss button
if (($ActionButtonEnabled -ne "True") -AND ($DismissButtonEnabled -ne "True")) {
Write-Log -Message "Creating the xml for no action button and no dismiss button"
[xml]$Toast = @"
<toast scenario="$Scenario">
<visual>
<binding template="ToastGeneric">
<image placement="hero" src="$HeroImage"/>
<image id="1" placement="appLogoOverride" hint-crop="circle" src="$LogoImage"/>
<text placement="attribution">$AttributionText</text>
<text>$HeaderText</text>
<group>
<subgroup>
<text hint-style="title" hint-wrap="true" >$TitleText</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$BodyText1</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$BodyText2</text>
</subgroup>
</group>
</binding>
</visual>
<actions>
</actions>
</toast>
"@
}
# Action button and NO dismiss button
if (($ActionButtonEnabled -eq "True") -AND ($DismissButtonEnabled -ne "True")) {
Write-Log -Message "Creating the xml for no dismiss button"
[xml]$Toast = @"
<toast scenario="$Scenario">
<visual>
<binding template="ToastGeneric">
<image placement="hero" src="$HeroImage"/>
<image id="1" placement="appLogoOverride" hint-crop="circle" src="$LogoImage"/>
<text placement="attribution">$AttributionText</text>
<text>$HeaderText</text>
<group>
<subgroup>
<text hint-style="title" hint-wrap="true" >$TitleText</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$BodyText1</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$BodyText2</text>
</subgroup>
</group>
</binding>
</visual>
<actions>
<action activationType="protocol" arguments="$Action" content="$ActionButtonContent" />
</actions>
</toast>
"@
}
# Dismiss button and NO action button
if (($ActionButtonEnabled -ne "True") -AND ($DismissButtonEnabled -eq "True")) {
Write-Log -Message "Creating the xml for no action button"
[xml]$Toast = @"
<toast scenario="$Scenario">
<visual>
<binding template="ToastGeneric">
<image placement="hero" src="$HeroImage"/>
<image id="1" placement="appLogoOverride" hint-crop="circle" src="$LogoImage"/>
<text placement="attribution">$AttributionText</text>
<text>$HeaderText</text>
<group>
<subgroup>
<text hint-style="title" hint-wrap="true" >$TitleText</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$BodyText1</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$BodyText2</text>
</subgroup>
</group>
</binding>
</visual>
<actions>
<action activationType="system" arguments="dismiss" content="$DismissButtonContent"/>
</actions>
</toast>
"@
}
# Snooze button - this option will always enable both action button and dismiss button regardless of config settings
if ($SnoozeButtonEnabled -eq "True") {
Write-Log -Message "Creating the xml for snooze button"
[xml]$Toast = @"
<toast scenario="$Scenario">
<visual>
<binding template="ToastGeneric">
<image placement="hero" src="$HeroImage"/>
<image id="1" placement="appLogoOverride" hint-crop="circle" src="$LogoImage"/>
<text placement="attribution">$AttributionText</text>
<text>$HeaderText</text>
<group>
<subgroup>
<text hint-style="title" hint-wrap="true" >$TitleText</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$BodyText1</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$BodyText2</text>
</subgroup>
</group>
</binding>
</visual>
<actions>
<input id="snoozeTime" type="selection" title="$SnoozeText" defaultInput="15">
<selection id="15" content="15 $MinutesText"/>
<selection id="30" content="30 $MinutesText"/>
<selection id="60" content="1 $HourText"/>
<selection id="240" content="4 $HoursText"/>
<selection id="480" content="8 $HoursText"/>
</input>
<action activationType="protocol" arguments="$Action" content="$ActionButtonContent" />
<action activationType="system" arguments="snooze" hint-inputId="snoozeTime" content="$SnoozeButtonContent"/>
<action activationType="system" arguments="dismiss" content="$DismissButtonContent"/>
</actions>
</toast>
"@
}
# Add an additional group and text to the toast xml used for notifying about possible deadline.
if (($DeadlineEnabled -eq "True") -OR ($DynDeadlineEnabled -eq "True")) {
if ($DeadlineContent) {
# Format the date time to match local culture of the running OS. Thanks @osdsune.com
$LocalCulture = Get-Culture
$RegionDateFormat = [System.Globalization.CultureInfo]::GetCultureInfo($LocalCulture.LCID).DateTimeFormat.LongDatePattern
$RegionTimeFormat = [System.Globalization.CultureInfo]::GetCultureInfo($LocalCulture.LCID).DateTimeFormat.ShortTimePattern
$LocalDateFormat = $DeadlineContent
$LocalDateFormat = Get-Date $LocalDateFormat -f "$RegionDateFormat $RegionTimeFormat"
$DeadlineGroup = @"
<group>
<subgroup>
<text hint-style="base" hint-align="left">$DeadlineText</text>
<text hint-style="caption" hint-align="left">$LocalDateFormat</text>
</subgroup>
</group>
"@
$Toast.toast.visual.binding.InnerXml = $Toast.toast.visual.binding.InnerXml + $DeadlineGroup
}
}
# Add an additional group and text to the toast xml for PendingRebootCheck
if ($PendingRebootCheckTextEnabled -eq "True") {
$PendingRebootGroup = @"
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$PendingRebootCheckTextValue</text>
</subgroup>
</group>
"@
$Toast.toast.visual.binding.InnerXml = $Toast.toast.visual.binding.InnerXml + $PendingRebootGroup
}
# Add an additional group and text to the toast xml for ADpasswordExpiration
if ($ADPasswordExpirationTextEnabled -eq "True") {
$ADPasswordExpirationGroup = @"
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$ADPasswordExpirationTextValue $ADPasswordExpirationDate</text>
</subgroup>
</group>
"@
$Toast.toast.visual.binding.InnerXml = $Toast.toast.visual.binding.InnerXml + $ADPasswordExpirationGroup
}
# Add an additional group and text to the toast xml used for notifying about computer uptime. Only add this if the computer uptime exceeds MaxUptimeDays.
if (($PendingRebootUptimeTextEnabled -eq "True") -AND ($Uptime -gt $MaxUptimeDays)) {
$UptimeGroup = @"
<group>
<subgroup>
<text hint-style="body" hint-wrap="true" >$PendingRebootUptimeTextValue</text>
</subgroup>
</group>
<group>
<subgroup>
<text hint-style="base" hint-align="left">$ComputerUptimeText $Uptime $ComputerUptimeDaysText</text>
</subgroup>
</group>
"@
$Toast.toast.visual.binding.InnerXml = $Toast.toast.visual.binding.InnerXml + $UptimeGroup
}
# Running the Display-notification function depending on selections and variables
# Toast used for upgrading OS. Checking running OS buildnumber. No need to display toast, if the OS is already running on TargetOS
if (($UpgradeOS -eq "True") -AND ($RunningOS.BuildNumber -lt $TargetOS)) {
Write-Log -Message "Toast notification is used in regards to OS upgrade. Taking running OS build into account"
Display-ToastNotification
# Stopping script. No need to accidently run further toasts
break
}
else {
Write-Log -Level Warn -Message "Conditions for displaying toast notifications for UpgradeOS are not fulfilled"
}
# Toast used for PendingReboot check and considering OS uptime
if (($PendingRebootUptime -eq "True") -AND ($Uptime -gt $MaxUptimeDays)) {
Write-Log -Message "Toast notification is used in regards to pending reboot. Uptime count is greater than $MaxUptimeDays"
Display-ToastNotification
# Stopping script. No need to accidently run further toasts
break
}
else {
Write-Log -Level Warn -Message "Conditions for displaying toast notifications for pending reboot uptime are not fulfilled"
}
# Toast used for pendingReboot check and considering checks in registry
if (($PendingRebootCheck -eq "True") -AND ($TestPendingRebootRegistry -eq $True)) {
Write-Log -Message "Toast notification is used in regards to pending reboot registry. TestPendingRebootRegistry returned $TestPendingRebootRegistry"
Display-ToastNotification
# Stopping script. No need to accidently run further toasts
break
}
else {
Write-Log -Level Warn -Message "Conditions for displaying toast notifications for pending reboot registry are not fulfilled"
}
# Toast used for pendingReboot check and considering checks in WMI
if (($PendingRebootCheck -eq "True") -AND ($TestPendingRebootWMI -eq $True)) {
Write-Log -Message "Toast notification is used in regards to pending reboot WMI. TestPendingRebootWMI returned $TestPendingRebootWMI"
Display-ToastNotification
# Stopping script. No need to accidently run further toasts
break
}
else {
Write-Log -Level Warn -Message "Conditions for displaying toast notifications for pending reboot WMI are not fulfilled"
}
# Toast used for ADPasswordExpiration
if (($ADPasswordExpiration -eq "True") -AND ($ADPasswordExpirationResult -eq $True)) {
Write-Log -Message "Toast notification is used in regards to ADPasswordExpiration. ADPasswordExpirationResult returned $ADPasswordExpirationResult"
Display-ToastNotification
# Stopping script. No need to accidently run further toasts
break
}
else {
Write-Log -Level Warn -Message "Conditions for displaying toast notification for ADPasswordExpiration are not fulfilled"
}
# Toast not used for either OS upgrade or Pending reboot OR ADPasswordExpiration. Run this if all features are set to false in config.xml
if (($UpgradeOS -ne "True") -AND ($PendingRebootCheck -ne "True") -AND ($PendingRebootUptime -ne "True") -AND ($ADPasswordExpiration -ne "True")) {
Write-Log -Message "Toast notification is not used in regards to OS upgrade OR Pending Reboots OR ADPasswordExpiration. Displaying default toast"
Display-ToastNotification
# Stopping script. No need to accidently run further toasts
break
}
else {
Write-Log -Level Warn -Message "Conditions for displaying default toast notification are not fulfilled"
}