Public/Add-VBUserPrinter.ps1

# ============================================================
# FUNCTION : Add-VBUserPrinter
# MODULE : VB.WorkstationReport
# VERSION : 1.0.0
# CHANGED : 23-04-2026 -- Initial release
# AUTHOR : Vibhu Bhatnagar
# PURPOSE : Adds a new UNC or IP printer to all or targeted user profiles
# ENCODING : UTF-8 with BOM
# ------------------------------------------------------------
# CHANGELOG (last 3-5 only -- full history in Git)
# v1.0.0 -- 23-04-2026 -- Initial release
# ============================================================

function Add-VBUserPrinter {
    <#
    .SYNOPSIS
        Adds a new UNC or IP printer to all or targeted user profiles on a machine.
 
    .DESCRIPTION
        Add-VBUserPrinter injects a printer mapping into user registry hives without
        requiring an existing printer to replace. It is the complement to
        Set-VBUserPrinterMigration, which only operates on printers already mapped
        in a user profile.
 
        For each target user profile the function:
          1. Mounts the NTUSER.DAT hive if not already loaded (via Mount-VBUserHive)
          2. Checks whether the printer is already present (idempotent -- skips if found)
          3. Writes the appropriate registry entries under HKU\{SID}
          4. Optionally sets the printer as the user's default
          5. Safely dismounts the hive (via Dismount-VBUserHive)
 
        For IP destinations, a machine-level TCP/IP port and printer are created before
        the per-user loop runs. This step requires the script to execute locally on the
        target machine (e.g. deployed via RMM). A terminating error is thrown if an IP
        printer is requested against a remote ComputerName target.
 
        Registry keys written per user:
          HKU\{SID}\Printers\Connections\,,server,share -- UNC connection marker
          HKU\{SID}\Software\...\Devices -- printer device list
          HKU\{SID}\Software\...\PrinterPorts -- printer port list (if present)
          HKU\{SID}\Software\...\Windows (Device value) -- default printer (if -SetAsDefault)
 
        A user logoff/logon may be required for changes to take full effect in active sessions.
 
    .PARAMETER PrinterPath
        UNC path (\\server\printer) or IP address (e.g. 10.30.1.50) of the printer to add.
 
    .PARAMETER PrinterName
        Display name for the printer as it appears in the user's Devices list.
        Required for IP printers. For UNC printers defaults to the full UNC path.
        Must match the machine-level printer name when adding IP printers.
 
    .PARAMETER DriverName
        Printer driver name. Required when PrinterPath is an IP address.
        The driver must already be installed on the machine.
        Example: 'HP LaserJet 400 M401', 'Canon Generic Plus PCL6'
        Run: Get-PrinterDriver | Select-Object Name to list installed drivers.
 
    .PARAMETER TargetUser
        Username or SID of a specific user to target. When omitted all non-system
        user profiles on the machine are processed.
 
    .PARAMETER SetAsDefault
        When specified, sets the added printer as the user's default printer.
 
    .PARAMETER ComputerName
        Target computer(s). Accepts pipeline input. Defaults to local machine.
        Remote targets are supported for user registry changes only.
        IP printer port/printer creation requires local execution.
 
    .PARAMETER Credential
        Credentials for remote execution. Not required for local or domain-joined targets.
 
    .EXAMPLE
        # -------------------------------------------------------------------
        # Adding an IP printer to all users on the local machine
        # -------------------------------------------------------------------
        # Step 1 -- Confirm the driver name on the target machine
        Get-PrinterDriver | Select-Object Name
        # Example output: HP LaserJet 400 M401
 
        # Step 2 -- Add the printer to all user profiles
        Add-VBUserPrinter -PrinterPath '10.30.1.55' `
                          -PrinterName 'HP_Accounts' `
                          -DriverName 'HP LaserJet 400 M401'
 
    .EXAMPLE
        # Adding a UNC printer to all user profiles (no DriverName needed)
        Add-VBUserPrinter -PrinterPath '\\PrintServer02\Canon_Reception'
 
    .EXAMPLE
        # Add an IP printer to a single user and set it as their default
        Add-VBUserPrinter -PrinterPath '10.30.1.55' `
                          -PrinterName 'HP_Accounts' `
                          -DriverName 'HP LaserJet 400 M401' `
                          -TargetUser 'jdoe' `
                          -SetAsDefault
 
    .EXAMPLE
        # Dry run -- see what would be added without making any changes
        Add-VBUserPrinter -PrinterPath '10.30.1.55' `
                          -PrinterName 'HP_Accounts' `
                          -DriverName 'HP LaserJet 400 M401' `
                          -WhatIf
 
    .EXAMPLE
        # Capture results and export to CSV for RMM reporting
        $results = Add-VBUserPrinter -PrinterPath '\\PrintServer02\Canon_Reception'
        $results | Export-Csv -Path "\\FileServer\Logs\AddPrinter_$env:COMPUTERNAME.csv" `
            -NoTypeInformation -Encoding UTF8
 
        # Review what was skipped (already had the printer)
        $results | Where-Object { $_.Action -eq 'AlreadyExists' }
 
    .EXAMPLE
        # Add a UNC printer to a specific user by SID
        Add-VBUserPrinter -PrinterPath '\\PrintServer02\Finance_HP' `
                          -TargetUser 'S-1-5-21-3456789012-1234567890-123456789-1001'
 
    .OUTPUTS
        PSCustomObject
        Returns one object per user profile processed:
          - ComputerName : Target computer
          - Username : User profile name
          - SID : User SID
          - PrinterPath : UNC path or IP address supplied
          - PrinterName : Display name written to the user's Devices key
          - Action : 'Added', 'AlreadyExists', or 'Failed'
          - SetAsDefault : True if printer was set as default for this user
          - Status : 'Success' or 'Failed'
          - Error : Error message (only present on failure)
          - Timestamp : Time of action (dd-MM-yyyy HH:mm:ss)
 
    .NOTES
        Version : 1.0.0
        Author : Vibhu Bhatnagar
        Category : Printer Management
 
        Requirements:
        - PowerShell 5.1
        - Administrative privileges
        - PrintManagement module (built-in on Windows 8 / Server 2012+)
        - Printer driver already installed for IP destinations
        - Script must run locally on target machine for IP printer port additions
 
        Related functions:
        - Set-VBUserPrinterMigration -- replaces existing printer mappings
        - Get-VBUserPrinterMappings -- audits current printer mappings per user
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string]$PrinterPath,

        [string]$PrinterName,

        [string]$DriverName,

        [string]$TargetUser,

        [switch]$SetAsDefault,

        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('Name', 'Server')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [PSCredential]$Credential
    )

    begin {
        $ErrorActionPreference = 'Stop'

        # --- Normalize and validate PrinterPath ---
        $PrinterPath = ($PrinterPath -replace '/', '\').TrimEnd('\').Trim()
        $isUNC       = $PrinterPath -match '^\\\\'
        $isIP        = $PrinterPath -match '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'

        if (-not $isUNC -and -not $isIP) {
            throw "PrinterPath must be a UNC path (\\server\printer) or an IP address. Received: '$PrinterPath'"
        }

        if ($isIP -and -not $DriverName) {
            throw "DriverName is required when PrinterPath is an IP address. Run: Get-PrinterDriver | Select-Object Name"
        }

        if ($isIP -and -not $PrinterName) {
            throw "PrinterName is required when PrinterPath is an IP address. This becomes the display name in the user's printer list."
        }

        # --- Resolve display name and port name ---
        $resolvedPrinterName = if ($PrinterName) { $PrinterName } else { $PrinterPath }
        $portName            = if ($isIP) { "IP_$PrinterPath" } else { $null }

        # --- Pre-compute registry values ---
        $connectionKeyName = $null
        $deviceValue       = $null

        if ($isUNC) {
            # \\server\printer -> ,,server,printer
            $connectionKeyName = ',,' + ($PrinterPath.TrimStart('\') -replace '\\', ',')
            $deviceValue       = 'winspool,Ne00:'
        }
        else {
            $deviceValue = "winspool,$portName"
        }
    }

    process {
        foreach ($computer in $ComputerName) {
            try {
                # --- Step 1: Machine-level port and printer for IP destinations (local only) ---
                if ($isIP) {
                    if ($computer -ne $env:COMPUTERNAME) {
                        throw "Machine-level printer port additions are not supported for remote targets. Run this script directly on '$computer' via RMM."
                    }

                    if (-not (Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue)) {
                        if ($PSCmdlet.ShouldProcess($portName, 'Add TCP/IP printer port')) {
                            Add-PrinterPort -Name $portName -PrinterHostAddress $PrinterPath -ErrorAction Stop
                            Write-Verbose "Added printer port: $portName"
                        }
                    }

                    if (-not (Get-Printer -Name $resolvedPrinterName -ErrorAction SilentlyContinue)) {
                        if ($PSCmdlet.ShouldProcess($resolvedPrinterName, 'Add printer')) {
                            Add-Printer -Name $resolvedPrinterName -PortName $portName -DriverName $DriverName -ErrorAction Stop
                            Write-Verbose "Added printer: $resolvedPrinterName on port $portName"
                        }
                    }
                }

                # --- Step 2: Resolve target user profiles ---
                $profileParams = @{ ErrorAction = 'Stop' }

                if ($computer -ne $env:COMPUTERNAME) {
                    $profileParams['ComputerName'] = $computer
                    if ($Credential) { $profileParams['Credential'] = $Credential }
                }

                $profiles = Get-VBUserProfile @profileParams

                if ($TargetUser) {
                    $profiles = @($profiles | Where-Object {
                        $_.Username -eq $TargetUser -or $_.SID -eq $TargetUser
                    })

                    if ($profiles.Count -eq 0) {
                        Write-Warning "No profile found for TargetUser '$TargetUser' on $computer"
                        continue
                    }
                }

                # --- Step 3: Per-user registry injection ---
                foreach ($profile in $profiles) {

                    $mountParams = @{ SID = $profile.SID; ErrorAction = 'Stop' }

                    if ($computer -ne $env:COMPUTERNAME) {
                        $mountParams['ComputerName'] = $computer
                        if ($Credential) { $mountParams['Credential'] = $Credential }
                    }

                    $mountResult = Mount-VBUserHive @mountParams

                    if ($mountResult.Status -ne 'Success') {
                        [PSCustomObject]@{
                            ComputerName = $computer
                            Username     = $profile.Username
                            SID          = $profile.SID
                            PrinterPath  = $PrinterPath
                            PrinterName  = $resolvedPrinterName
                            Action       = 'Failed'
                            SetAsDefault = $false
                            Status       = 'Failed'
                            Error        = "Hive mount failed: $($mountResult.Error)"
                            Timestamp    = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')
                        }
                        continue
                    }

                    try {
                        $regBase     = "Registry::HKEY_USERS\$($mountResult.SID)"
                        $connectPath = "$regBase\Printers\Connections"
                        $devicesPath = "$regBase\Software\Microsoft\Windows NT\CurrentVersion\Devices"
                        $portsPath   = "$regBase\Software\Microsoft\Windows NT\CurrentVersion\PrinterPorts"
                        $windowsPath = "$regBase\Software\Microsoft\Windows NT\CurrentVersion\Windows"

                        # --- Idempotency check ---
                        $alreadyExists = $false

                        if ($isUNC) {
                            $alreadyExists = Test-Path -Path "$connectPath\$connectionKeyName"
                        }
                        else {
                            $deviceProps = Get-ItemProperty -Path $devicesPath -ErrorAction SilentlyContinue
                            if ($deviceProps) {
                                $members = $deviceProps |
                                    Get-Member -MemberType NoteProperty |
                                    Where-Object { $_.Name -notlike 'PS*' }
                                foreach ($member in $members) {
                                    if ($deviceProps.($member.Name) -like "*$portName*") {
                                        $alreadyExists = $true
                                        break
                                    }
                                }
                            }
                        }

                        if ($alreadyExists) {
                            [PSCustomObject]@{
                                ComputerName = $computer
                                Username     = $profile.Username
                                SID          = $profile.SID
                                PrinterPath  = $PrinterPath
                                PrinterName  = $resolvedPrinterName
                                Action       = 'AlreadyExists'
                                SetAsDefault = $false
                                Status       = 'Success'
                                Timestamp    = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')
                            }
                            continue
                        }

                        if (-not $PSCmdlet.ShouldProcess("$($profile.Username) on $computer", "Add printer $resolvedPrinterName")) {
                            continue
                        }

                        # --- Add UNC Connections subkey ---
                        if ($isUNC) {
                            if (-not (Test-Path -Path $connectPath)) {
                                New-Item -Path $connectPath -Force | Out-Null
                            }
                            New-Item -Path $connectPath -Name $connectionKeyName -Force | Out-Null
                        }

                        # --- Add Devices entry ---
                        if (Test-Path -Path $devicesPath) {
                            Set-ItemProperty -Path $devicesPath -Name $resolvedPrinterName -Value $deviceValue -Type String -Force
                        }

                        # --- Add PrinterPorts entry (only if key exists) ---
                        if (Test-Path -Path $portsPath) {
                            $portsValue = if ($deviceValue -notlike '*,15,45') { "$deviceValue,15,45" } else { $deviceValue }
                            Set-ItemProperty -Path $portsPath -Name $resolvedPrinterName -Value $portsValue -Type String -Force
                        }

                        # --- Set as default if requested ---
                        $wasSetAsDefault = $false
                        if ($SetAsDefault) {
                            $defaultValue = "$resolvedPrinterName,$deviceValue"
                            Set-ItemProperty -Path $windowsPath -Name 'Device' -Value $defaultValue -Type String -Force
                            $wasSetAsDefault = $true
                        }

                        [PSCustomObject]@{
                            ComputerName = $computer
                            Username     = $profile.Username
                            SID          = $profile.SID
                            PrinterPath  = $PrinterPath
                            PrinterName  = $resolvedPrinterName
                            Action       = 'Added'
                            SetAsDefault = $wasSetAsDefault
                            Status       = 'Success'
                            Timestamp    = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')
                        }
                    }
                    catch {
                        [PSCustomObject]@{
                            ComputerName = $computer
                            Username     = $profile.Username
                            SID          = $profile.SID
                            PrinterPath  = $PrinterPath
                            PrinterName  = $resolvedPrinterName
                            Action       = 'Failed'
                            SetAsDefault = $false
                            Status       = 'Failed'
                            Error        = $_.Exception.Message
                            Timestamp    = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')
                        }
                    }
                    finally {
                        $mountResult | Dismount-VBUserHive | Out-Null
                    }
                }
            }
            catch {
                [PSCustomObject]@{
                    ComputerName = $computer
                    Username     = 'N/A'
                    SID          = 'N/A'
                    PrinterPath  = $PrinterPath
                    PrinterName  = $resolvedPrinterName
                    Action       = 'Failed'
                    SetAsDefault = $false
                    Status       = 'Failed'
                    Error        = $_.Exception.Message
                    Timestamp    = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')
                }
            }
        }
    }
}