DSCResources/MSFT_xTokenize/MSFT_xTokenize.psm1
# Default strings if a culture specific file cannot be imported DATA localizedStrings { # culture = "en-US" ConvertFrom-StringData @' ErrorRunningSetFunction = An error occurred while running Set-TargetResource function ErrorRunningTestFunction = An error occurred while running Test-TargetResource function Path = path = {0} Recursive = recurse = {0} SearchPattern = searchPattern = {0} UseTokenFiles = useTokenFiles = {0} Returned = Returned {0} ReplacingTokensParam = Replacing tokens in PSBoundParameters RemovingParameters = Removing parameters name and searchPattern from PSBoundParameters TestSingleFile = Test Single File Loading = Loading {0} NoTokensReplaced = No tokens were replaced TokensReplaced = Tokens replaced NoTokenFile = No token file found TestDirectory = Test Directory Filter = filter = {0} ProcessDirectory = Process Directory ProcessSingleFile = Process Single File Processing = Processing {0} CouldNotRead = Could not read contents of file Saving = Saving {0} ProcessingComplete = Processing complete ReplacingTokens = Replacing Tokens NullTokens = Tokens is null Replacing = Replacing {0} with {1} ReplacingFilter = Replacing filter {0} with {0}.token GetFiles = Get Files BuildHashtable = Build hashtable HashEntry = Key: {0}, Value: {1} '@ } # Load localized strings Import-LocalizedData -BindingVariable localizedStrings -FileName MSFT_xTokenize.psd1 -ErrorAction:SilentlyContinue function Get-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [parameter(Mandatory = $true)] [string] $path, [bool] $recurse, [string] $searchPattern = "*.*", [Microsoft.Management.Infrastructure.CimInstance[]] $tokens, [bool] $useTokenFiles = $true ) return @{ Path = $path Recurse = $recurse SearchPattern = $searchPattern Tokens = $tokens UseTokenFiles = $useTokenFiles } } function Set-TargetResource { [CmdletBinding()] param ( [parameter(Mandatory = $true)] [string] $path, [bool] $recurse, [string] $searchPattern = "*.*", [Microsoft.Management.Infrastructure.CimInstance[]] $tokens, [bool] $useTokenFiles = $true ) try { # Convert array into hashtable $tokensHashTable = ToHashtable $tokens # We have to remove name before splatting to ProcessDirectory # Pipe to Out-Null so it does not write the value of name to the # screen Write-Debug $localizedStrings.RemovingParameters $PSBoundParameters.Remove("name") | Out-Null $PSBoundParameters.Remove("searchPattern") | Out-Null $PSBoundParameters.Add("filter", $searchPattern) # We also need to replace our tokens value with what we created # instead Write-Debug $localizedStrings.ReplacingTokensParam $PSBoundParameters["tokens"] = $tokensHashTable ProcessDirectory @PSBoundParameters } catch { $exception = $_ Write-Verbose $localizedStrings.ErrorRunningSetFunction while($exception.InnerException -ne $null) { $exception = $exception.InnerException if($exception.message -ne $null) { Write-Verbose $exception.message } } } } function Test-TargetResource { [CmdletBinding()] [OutputType([System.Boolean])] param ( [parameter(Mandatory = $true)] [string] $path, [bool] $recurse, [string] $searchPattern = "*.*", [Microsoft.Management.Infrastructure.CimInstance[]] $tokens, [bool] $useTokenFiles = $true ) try { Write-Debug ($localizedStrings.Path -f $path) Write-Debug ($localizedStrings.Recursive -f $recurse) Write-Debug ($localizedStrings.SearchPattern -f $searchPattern) Write-Debug ($localizedStrings.UseTokenFiles -f $useTokenFiles) # Convert array into hashtable $tokensHashTable = ToHashtable $tokens # We have to remove name before splatting to ProcessDirectory # Pipe to Out-Null so it does not write the value of name to the # screen Write-Debug $localizedStrings.RemovingParameters $PSBoundParameters.Remove("name") | Out-Null $PSBoundParameters.Remove("searchPattern") | Out-Null $PSBoundParameters.Add("filter", $searchPattern) # We also need to replace our tokens value with what we created # instead Write-Debug $localizedStrings.ReplacingTokensParam $PSBoundParameters["tokens"] = $tokensHashTable $result = TestDirectory @PSBoundParameters Write-Verbose ($localizedStrings.Returned -f $result) return $result } catch { $exception = $_ Write-Verbose $localizedStrings.ErrorRunningTestFunction while($exception.InnerException -ne $null) { $exception = $exception.InnerException if($exception.message -ne $null) { Write-Verbose $exception.message } } } } # The build process replaces the target file with the token file contents. So the # drop location will have two identical files before the transformation. # The goal is the load the target file and load and transform the token file. If the # contents of the target file match the value of the transformed file or we can't find # the token file return true. In all other cases return false. function TestSingleFile { # Use CmdletBinding to pass any -verbose flags into this function [CmdletBinding()] param ( [string] $path, [hashtable] $tokens, [bool] $useTokenFiles ) Write-Verbose $localizedStrings.TestSingleFile Write-Debug ($localizedStrings.UseTokenFiles -f $useTokenFiles) Write-Debug ($localizedStrings.Path -f $path) $result = $false # Assume they are not using a token file. And if so the finalFilename # and the provided path are the same. If a token file was used that will # be taken care of later. $finalFileName = $path $tokenFile = $path # Only remove the .token if usetokenFiles is true. There is an # edge condition where removing it could overwrite a file with # the same name minus .token that was not to be touched. if($useTokenFiles) { $finalFileName = $path -replace "\.token", "" } $file = Get-Item -Path $finalFileName if(Test-Path -Path $tokenFile) { $contents = Get-Content -Path $finalFileName if($useTokenFiles) { Write-Verbose ($localizedStrings.Loading -f $tokenFile) $tokenContents = Get-Content -Path $tokenFile $processedContents = ReplaceTokens -contents $tokenContents -tokens $tokens if("$processedContents" -eq "$tokens") { Write-Verbose $localizedStrings.NoTokensReplaced } else { Write-Verbose $localizedStrings.TokensReplaced } # To test when you are using a token file simply load the # target file off disk and process the token file. If they match # after return true. # Testing this way gives you the added advantage of being able to # just push a configuration change. Because the token file is never # changed we could process it with new values and compare it to the # existing target file (even if the target file had been previously # processed) and see if they match. If they don't return false. $result = "$contents" -eq "$processedContents" Write-Verbose ($localizedStrings.Returned -f $result) } else { # If you are not using a token file all we can do is make sure # we can't find any of the tokens in the target file. # Assume all is well $result = $true foreach($token in $tokens.Keys) { $toFind = "*__$($token)__*" if($contents -like $toFind) { $result = $false break } } } } else { Write-Verbose $localizedStrings.NoTokenFile $result = $true } $result } function TestDirectory { # Use CmdletBinding to pass any -verbose flags into this function [CmdletBinding()] param ( [string] $path, [string] $filter, [hashtable] $tokens, [bool] $recurse, [bool] $useTokenFiles ) Write-Debug $localizedStrings.TestDirectory Write-Debug ($localizedStrings.Filter -f $filter) Write-Debug ($localizedStrings.Path -f $path) Write-Debug ($localizedStrings.Recursive -f $recurse) Write-Debug ($localizedStrings.UseTokenFiles -f $useTokenFiles) $result = $true # We have to remove tokens before splatting to GetFiles # Pipe to Out-Null so it does not write the value of useTokenFiles to the # screen $PSBoundParameters.Remove("tokens") | Out-Null $files = GetFiles @PSBoundParameters if($files.Count -ne 0) { foreach($file in $files) { Write-Verbose $file if((TestSingleFile -path $file.FullName -tokens $tokens -useTokenFiles $useTokenFiles) -eq $false) { $result = $false break; } } } return $result } function ProcessDirectory { # Use CmdletBinding to pass any -verbose flags into this function [CmdletBinding()] param ( [string] $path, [string] $filter, [hashtable] $tokens, [bool] $recurse, [bool] $useTokenFiles ) Write-Debug $localizedStrings.ProcessDirectory # We have to remove tokens before splatting to GetFiles # Pipe to Out-Null so it does not write the value of useTokenFiles to the # screen $PSBoundParameters.Remove("tokens") | Out-Null $files = GetFiles @PSBoundParameters foreach($file in $files) { ProcessFile -path $file.FullName -tokens $tokens } } # Takes in the arguments from the Resource and makes sure they # are prepared for the call to ProcessFile. function ProcessSingleFile { # Use CmdletBinding to pass any -verbose flags into this function [CmdletBinding()] param ( [string] $path, [hashtable] $tokens, [bool] $useTokenFiles ) Write-Debug $localizedStrings.ProcessSingleFile Write-Debug ($localizedStrings.Path -f $path) Write-Debug ($localizedStrings.UseTokenFiles -f $useTokenFiles) $file = Get-Item -Path $path $tokenFile = $path if($useTokenFiles) { $tokenFile = "$($path).token" } if(Test-Path -Path $tokenFile) { ProcessFile -path $tokenFile -tokens $tokens } else { Write-Verbose $localizedStrings.NoTokenFile } } # This is where all the real work gets done. # Takes a single file replaces the tokens and writes the output. # It also takes care of removing and resetting the readonly flag # on the target file. function ProcessFile { # Use CmdletBinding to pass any -verbose flags into this function [CmdletBinding()] param ( [string] $path, [hashtable] $tokens ) Write-Verbose ($localizedStrings.Processing -f $path) $contents = Get-Content -Path $path if($contents -eq $null) { Write-Warning $localizedStrings.CouldNotRead return } $result = ReplaceTokens -contents $contents -tokens $tokens # Assume they are not using a token file. And if so the finalFilename # and the provided path are the same. If a token file was used that will # be taken care of later. $finalFileName = $path # Only remove the .token if usetokenFiles is true. There is an # edge condition where removing it could overwrite a file with # the same name minus .token that was not to be touched. if($useTokenFiles) { $finalFileName = $path -replace "\.token", "" } Write-Verbose ($localizedStrings.Saving -f $finalFileName) $file = Get-Item -Path $finalFileName $originalAttributes = $file.Attributes $desiredAttributes = RemoveReadyOnlyAttribute($originalAttributes) $file.Attributes = $desiredAttributes Set-Content -Path $finalFileName -Value $result $file.Attributes = $originalAttributes Write-Verbose $localizedStrings.ProcessingComplete } # This method simply finds and replaces all the tokens in the provided contents. # The tokens provided in the hashtable do not contain the underscores so those # need to be added to the keys of the hashtable before they are searched for in # the contents. function ReplaceTokens { # Use CmdletBinding to pass any -verbose flags into this function [CmdletBinding()] param ( [object[]] $contents, [hashtable] $tokens ) Write-Verbose $localizedStrings.ReplacingTokens $result = $contents if($tokens -eq $null) { Write-Verbose $localizedStrings.NullTokens return $result } foreach($key in $($tokens.Keys)) { $value = $tokens[$key] $token = "__$($key)__" Write-Verbose ($localizedStrings.Replacing -f $token, $value) # Now replace all instances of token with value $result = $result -replace $token, $value } return $result } # When files are dropped into the drop location the ReadyOnly bit can be set. If that # is the case it must be removed before the file can be transformed. This method makes # it easy to only remove the ReadOnly bit leaving all the other attributes intact. Once # the file is transformed the original attributes should be returned. function RemoveReadyOnlyAttribute { # Use CmdletBinding to pass any -verbose flags into this function [CmdletBinding()] param ( [System.IO.FileAttributes] $attributes ) return ($attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly)) -as [System.IO.FileAttributes] } # This will return all the files that need to be transformed. # If the UseTokenFiles is present only files that match the # filter AND have a .token file as well will be returned. # If a folder contains # web.config # packages.config # web.config.token # and filter is *.config and UseTokenFiles is false both # web.config and packages.config would be returned. However, # if UseTokenFiles is true only web.config would be returned. function GetFiles { # Use CmdletBinding to pass any -verbose flags into this function [CmdletBinding()] param ( [string] $path, [string] $filter, [bool] $recurse, [bool] $useTokenFiles ) Write-Debug $localizedStrings.GetFiles Write-Debug ($localizedStrings.Path -f $path) Write-Debug ($localizedStrings.Filter -f $filter) Write-Debug ($localizedStrings.Recursive -f $recurse) Write-Debug ($localizedStrings.UseTokenFiles -f $useTokenFiles) # We have to remove useTokenFiles before splatting to Get-ChildItem # Pipe to Out-Null so it does not write the value of useTokenFiles to the # screen $PSBoundParameters.Remove("useTokenFiles") | Out-Null # We only want to return files not directories so add the File switch $PSBoundParameters.Add("file", $true) if($useTokenFiles) { Write-Verbose ($localizedStrings.ReplacingFilter -f $filter) $PSBoundParameters["filter"] = "$($filter).token" } # Return all files that match the filter if UseTokenFiles is false or only # files that have a .token file as well. Get-ChildItem @PSBoundParameters } # Converts a MSFT_KeyValuePair array into a PowerShell hashtable function ToHashtable { [CmdletBinding()] param ( [Microsoft.Management.Infrastructure.CimInstance[]] $tokens ) Write-Debug $localizedStrings.BuildHashtable $hashTable = @{} foreach($instance in $tokens) { $hashTable.Add($instance.Key, $instance.Value) Write-Debug ($localizedStrings.HashEntry -f $instance.Key, $instance.Value) } return $hashTable } Export-ModuleMember -Function *-TargetResource |