Parsing XML in Powershell Workflow - powershell-4.0

I'm trying to read data from an XML file in a Powershell Workflow.
In a normal Powershell script, I would load the file into an XML Object: [xml]$object = Get-Content $xmlPath. Workflow doesn't seem to like these objects though, and on any checkpoints, fails with the error:
The input objects cannot be serialized. Serialized data is required to suspend
and resume workflows. To resolve the error, verify that the values of all
variables and parameters are of types that can be serialized.
+ CategoryInfo : InvalidResult: (:) [], SerializationException
+ FullyQualifiedErrorId : JobStatusFailed
Does Powershell have another way of parsing XML that meshes well with Workflow?
Thanks

There are limitations for operations in Workflow, so, some PowerShell code should be put to InlineScript to avoid this limitations:
workflow Parse-Xml {
$xmlPath = "...."
$xml = InlineScript {
[xml] Get-Content $Using:xmlPath
}
$xml
}
Parse-Xml

Related

Create a potocol handler for a Powershell script

How can I create a protocol handler for a powershell script and make the target powershell script receive command line arguments?
And what are the security concerns in doing so?
I thought I write up a decent guide on doing so since the information I found online was lacking some details.
https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85)
First off the security concerns
Any program, website, script etc. that is running on your computer can set off the protocol. There are no authorization checks.
You should NOT create a universal protocol handler. That would be a massive security issue concern. I mean that would enable a program, website, script etc. to run any powershell script or command on your computer.
Creating the protocol handler in Windows registry
The protocol must be registered in Windows Registry. It's a simple task.
I'm calling my powershell protocol handler for pwsh
Step 1: Open the Registry Editor and navigate to
Computer\HKEY_CLASSES_ROOT
For inspiration you can look at Computer\HKEY_CLASSES_ROOT\http to look at how that protocol handler is made.
Step 2: Create the following hierarchy:
Create the key pwsh: [Computer\HKEY_CLASSES_ROOT\pwsh]
Edit the default value of (Default) to URL:pwsh. Remember I call my protocol handler for pwsh, write whatever your is called.
Add a string value with the name URL Protocol and empty data.
It should look like this now:
Create a new key under pwsh, DefaultIcon: Computer\HKEY_CLASSES_ROOT\pwsh\DefaultIcon.
Set the (Default) data field to a filepath that leads to an icon or image. I used the powershell icon for Powershell 7 C:\Program Files (x86)\PowerShell\7-preview\assets\ps_black_32x32.ico.
Then create the keys shell -> open -> command like shown on the image above.
In the key command change the (Default) data value to where powershell is installed and then the powershell script to be run.
When testing I do this: "C:\Program Files\PowerShell\6\pwsh.exe" -noexit -executionpolicy bypass -Command {Write-Host %1}
Note I am using powershell core 6 and your path to powershell is probably different.
You can test to check if it works by opening the run program in Windows(Windows+R).
Expected behavior is the powershell window to open with the text pwsh:Hello Stackoverflow printed.
Step 3: Create a powershell script to handle incoming actions on the protocol.
The production ready data value for the command key: "C:\Program Files\PowerShell\6\pwsh.exe" -noexit -File C:\handleActions.ps1 %1
Param($Argument="") # If the protocol is ran you always at least get the protocol name as an argument. (if using the %1)
[String]
$Argument
function Handle-Actions { # The cmdlet 'Handle-Actions' uses an unapproved verb.
[cmdletBinding()]
param(
[Parameter(Mandatory=$false, Position=0)]
[String]
$Argument
)
$Argumuments = $Argument.Split([Char]0x003F) # Splits by `?`
#Argumnets is now in an array, do whatever you need to next.
$Argumuments | %{
Write-Host $_ # Writes each argument that was seperated by ? to a line
}
}
Handle-Actions -Argument $Argument
Given the run command pwsh:?firstArgument?SecondArgument the script will output:
pwsh:
firstArgument
SecondArgument
To complement your helpful guide with sample code that automates creation of a custom protocol handler:
The following:
Creates a custom URI protocol custom: (rather than pwsh:, given that PowerShell is simply used to implement the protocol) to which an open-ended number of arguments can be passed.
Does so for the current user only (HKEY_CURRENT_USER\Software\Classes) by default; however, it's easy to tweak the code to implement the custom protocol for all users instead (HKEY_LOCAL_MACHINE\Software\Classes), though you'll need to run the code with elevation (as administrator) then.
A handler *.ps1 script is automatically created:
At $env:USERPROFILE\customUriHandler.ps1 in the current-user scenario.
At $env:ALLUSERPROFILE\customUriHandler.ps1 in the all-users scenario.
The handler script simply echoes the arguments passed to it, and it is invoked in a PowerShell script window that is kept open after script execution (-NoExit); tweak the PowerShell command as needed.
The protocol expects its arguments as if it were a shell command, i.e., as a space-separated list of arguments, with argument-individual "..." quoting, if necessary.
The sample command at the end uses Start-Process to invoke the following URI, which you could also submit from the Run dialog (WinKey-R), which passes arguments one, two & three, four:
URI: custom:one "two & three" four
Invocation via Start-Process: Start-Process 'custom:one "two & three" four'
Caveat: If you submit this URI via a web browser's address bar (note: doesn't seem to work with Microsoft Edge), it is URI-escaped, and a single one%20%22two%20&%20three%22%20four argument is passed instead, which would require custom parsing; similarly, submitting from File Explorer's address bar passes one%20two%20&%20three%20four, though note that the " chars. are - curiously - lost in the process.
# Determine the scope:
# Set to $false to install machine-wide (for all users)
# Note: Doing so then requires running with ELEVATION.
$currentUserOnly = $true
if (-not $currentUserOnly) {
net session *>$null
if ($LASTEXITCODE) { Throw "You must run this script as administrator (elevated)." }
}
$ErrorActionPreference = 'Stop'
# The name of the new protocol scheme
$schemeName = 'custom'
$pwshPathEscaped = (Get-Process -Id $PID).Path -replace '\\', '\\'
$handlerScript = ($env:ALLUSERSPROFILE, $env:USERPROFILE)[$currentUserOnly] + "\${schemeName}UriHandler.ps1"
$handlerScriptEscaped = $handlerScript -replace '\\', '\\'
# Create the protocol handler script.
#'
# Remove the protocol scheme name from the 1st argument.
$argArray = $args.Clone()
$argArray[0] = $argArray[0] -replace '^[^:]+:'
# If the 1st argument is now empty, remove it.
if ('' -eq $argArray[0]) { $argArray = $argArray[1..($argArray.Count-1)] }
"Received $($argArray.Count) argument(s)."
$i = 0
foreach ($arg in $argArray) {
"#$((++$i)): [$arg]"
}
'# > $handlerScript
# Construct a temp. *.reg file.
# Target the scope-appropriate root registrykey.
$rootKey = ('HKEY_LOCAL_MACHINE\Software\Classes', 'HKEY_CURRENT_USER\Software\Classes')[$currentUserOnly]
# Determine a temp. file path.
$tempFile = [IO.Path]::GetTempPath() + [IO.Path]::GetRandomFileName() + '.reg'
#"
Windows Registry Editor Version 5.00
[$rootKey\$schemeName]
#="URL:$schemeName"
"URL Protocol"=""
[$rootKey\$schemeName\DefaultIcon]
#="$pwshPathEscaped"
[$rootKey\$schemeName\shell]
#="open"
[$rootKey\$schemeName\shell\open\command]
; === Tweak the PowerShell command line here: ===
#="\"$pwshPathEscaped\" -ExecutionPolicy Bypass -NoProfile -NoExit -File \"$handlerScriptEscaped\" %1"
"# > $tempFile
# Import the *.reg file into the registry.
& {
$ErrorActionPreference = 'Continue'
reg.exe import $tempFile 2>$null
if ($LASTEXITCODE) { Throw "Importing with reg.exe failed: $tempFile"}
}
# Remove the temp. *.reg file.
Remove-Item -ErrorAction Ignore -LiteralPath $tempFile
# ---
# Sample invocation of the new protocol with 3 arguments:
$uri = "$schemeName`:one `"two & three`" four"
Write-Verbose -Verbose "Invoking the following URI: $uri"
Start-Process $uri

Admin vs Non-Admin Mode - Cannot overwrite variable because the variable has been optimized

During some testing today I came across an unexpected issue and I do not understand why it is happening. Below is the code I am using to duplicate the issue. It is only a very small portion of the larger project.
Testing is being cone on Windows 10 Build 1709, if that helps
Both the PS1 File and the BAT File are named the same.
Ways to cause the errors
Running the PS1 File via Right-Click - Run with PowerShell will cause the error
Opening PowerShell ISE in Non-Admin Mode, then opening/running the script will cause the error
Running BAT File as Admin or Non-Admin will cause the error
Ways to avoid the errors
Opening PowerShell ISE in Admin Mode, then opening/running the script will not cause the error
Adding Script: in front of the variables on the last 2 lines of code will not cause the error no matter how the script is executed
Using VSCode, it will work as shown below. Running it in the integrated terminal, it will see it not running as an Admin, it will launch PowerShell.exe outside of VSCode and work without issue
-
Why do I have Script: in front of the variables in the functions? It was the only way I could get variables set in the functions to be used outside the functions. The other 25 or so variables not listed in this post do not have an issue, however, they are not modified like these two are after they are set.
The Questions
Why, if running the ISE in Admin Mode, it will work?
Why would it not work if it relaunches as an Administrator?
Why does VSCode not care and it works regardless?
Something isn't making sense and I cannot pinpoint it.
Here are the errors
Cannot overwrite variable NetFX3 because the variable has been optimized. Try using the New-Variable or Set-Variable
cmdlet (without any aliases), or dot-source the command that you are using to set the variable.
At C:\Users\a502690530\Desktop\Testing2.ps1:14 char:5
+ [string]$Script:NetFX3 = $BAT_Files_Path + "NetFX3.zip"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : WriteError: (NetFX3:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritableRare
Cannot overwrite variable Power_Plan because the variable has been optimized. Try using the New-Variable or
Set-Variable cmdlet (without any aliases), or dot-source the command that you are using to set the variable.
At C:\Users\a502690530\Desktop\Testing2.ps1:15 char:5
+ [string]$Script:Power_Plan = $BAT_Files_Path + "Power_Plan.zip"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : WriteError: (Power_Plan:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritableRare
Here is the code
# Checks if running as an administrator. If not, it will relaunch as an administrator
If (-Not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
$Arguments = "& '" + $MyInvocation.MyCommand.Definition + "'"
Start-Process Powershell -Verb RunAs -ArgumentList $Arguments
Break
}
[string]$ErrorActionPreference = "Continue"
[string]$BAT_Files = $Root_Path + "BAT_Files\"
Function Set-FilePaths ([string]$BAT_Files_Path) {
# BAT Files Paths (ZIPs only!!!)
[string]$Script:NetFX3 = $BAT_Files_Path + "NetFX3.zip"
[string]$Script:Power_Plan = $BAT_Files_Path + "Power_Plan.zip"
Set-Lists
}
function Set-Lists {
# List of BAT Files (ZIPs)
[System.Collections.ArrayList]$Script:List_Of_BAT_Files = #(
$NetFX3
$Power_Plan
)
}
Set-FilePaths `
-BAT_Files_Path $BAT_Files
PAUSE
$NetFX3 = ((Split-Path $NetFX3 -Parent) + "\NetFX3\")
$Power_Plan = ((Split-Path $Power_Plan -Parent) + "\Power_Plan\")
BAT File to launch
REG ADD "HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" /T REG_SZ /V ExecutionPolicy /D Unrestricted /F
Start PowerShell.exe -Command "& '%~dpn0.ps1'"
I have no specific answer, but a pointer:
Your issue sounds like a PowerShell bug related to the DLR (Dynamic Language Runtime), a technology PowerShell uses behind the scenes (since v3); there's at least one open bug report on GitHub that sounds related.
Aside from the workaround you already know of - using scope modifier script consistently - I suggest avoiding variable access across scope boundaries as a general best practice, which should also avoid the problem.
PowerShell is very flexible in what it can return (output) from a function, so it's better to set variables in the caller's scope based on a function's output.
Specifically, I suggest refactoring your code as follows:
Function Get-FilePaths ([string]$BAT_Files_Path) {
# Output the paths as an *array*.
($BAT_Files_Path + "NetFX3.zip"), ($BAT_Files_Path + "Power_Plan.zip")
}
# Call the function in the script scope and capture its output in variables.
$List_Of_BAT_Files = Get-FilePaths
# Use a destructuring assignment to store the elements of the array
# in individual variables
$NetFX3, $Power_Plan = $List_Of_BAT_Files
If there are a lot of individual variables to set, you can make the function output a hash table instead, and use the hash table's named entries instead of individual variables (requires PSv3+, due to use of [ordered] to create a hash table with ordered keys):
Function Get-FilePaths ([string]$BAT_Files_Path) {
# Output the paths as a *hash table*, using its
# entries for named access instead of individual variables.
$outHash = [ordered] #{
NetFX3 = $BAT_Files_Path + "NetFX3.zip"
Power_Plan = $BAT_Files_Path + "Power_Plan.zip"
}
# Add a 'List' entry that contains all values added above as an array.
# Note the need to use #(...) to force creation of a new array from the
# hash table's value collection.
$outHash.List = #($outHash.Values)
# Output the hash table.
$outHash
}
# Call the function in the script scope and capture its output in
# a single variable that receives the hash table.
$hash = Get-FilePaths
# Now you can access the invididual values by name - e.g., $hash.NetFX3 -
# or use $hash.List to get all values.

What is causing this PowerShell script error?

I'm writing a PowerShell script that registers and unregistered Scheduled Tasks on Windows Server 2012 R2. I'm in particular having trouble with this line:
Unregister-ScheduledTask -TaskPath "\Sensei\"
(Sensei is the code name of this project, and is the name of the folder that contains all the tasks I care about.)
When I run that command from ISE or the command line, it works fine. However, when I run it from a .ps1 file, it gives me a downright confusing error:
Unregister-ScheduledTask : Cannot retrieve the dynamic parameters for the cmdlet. Cannot bind argument to parameter 'TypeName' because it is null.
At Z:\server_scripts\clean.ps1:12 char:5
+ Unregister-ScheduledTask -TaskPath "\Sensei\"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Unregister-ScheduledTask], ParameterBindingException
+ FullyQualifiedErrorId : GetDynamicParametersException,Unregister-ScheduledTask
As near as I can tell, TypeName is not a parameter that Unregister-ScheduledTask accepts, so it must be coming from somewhere internal to the cmdlet.
Does anyone have any idea what's causing this?
EDIT: Someone asked for the whole script. I omitted it since there's nothing else of note.
cd Z:\server_scripts
. .\variables.ps1
#Delete config files
del $SENSEIPATH\assets.cfg
del $SENSEIPATH\Web\Web.config
#Remove the virtual application
Remove-WebApplication -Site $IISSITE -Name "c"
#Clean out the scheduled tasks
Unregister-ScheduledTask -TaskPath "\Sensei\"
.\variables.ps1 just contains a few variable definitions, such as $SENSEIPATH or $IISSITE. I can't post it here, but I promise there's nothing fancy.
So, I don't understand what, exactly, the problem is, but here's what is going on.
I actually have a separate script, not listed above, that downloads the script in question, and then executes it. This is bootstrap.ps1. It looks something like this:
Read-S3Object -BucketName $BUCKET -Folder z:\server_scripts -KeyPrefix "server_scripts/app"
Z:\server_scripts\clean.ps1
When this ran from inside ISE, it would produce the error message above.
HOWEVER, if I ran clean.ps1 directly, or ran bootstrap.ps1 outside of ISE, OR I modified bootstrap.ps1 like this, it works fine:
Read-S3Object -BucketName $BUCKET -Folder z:\server_scripts -KeyPrefix "server_scripts/app"
. Z:\server_scripts\clean.ps1
(I missed that some of these were succeeding because it throws a different error if Unregister-ScheduledTask doesn't find any tasks to unschedule, and wasn't paying close attention).
So, there might be some kind of scoping issue, but I am long past caring, and just want this thing to work. May this help someone else!

Possible to extract Embedded image to a file?

Given a SSRS report definition file with an embedded image in it, just wondering if its possible to extract that image XML to recreate the original image file.
e.g. :
inside the rdlc file, you might see xml like this :
<EmbeddedImage Name="tick">
<MIMEType>image/bmp</MIMEType>
<ImageData>Qk1mAwAAAAAAADYAAAAoAAAAEAAAABEAAAABABgA ... <<REST OF IMAGE HERE>>
</ImageData>
</EmbeddedImage>
Is it possible to take the ImageData, and transform form it in some way to re-create the original image bitmap byte stream ?
(This might be useful in cases such as when you've lost the original image file on which the embedded image was based.)
Two approaches are detailed in this blog post:
Copy the encoded image from one report to another if you need to reuse it there.
Export a copy of the report to Excel and copy the image from the spreadsheet.
Or if you need access to the image more directly, I found this utility that will parse the XML and load and export the images. Looks like source code is available.
I have created a small Power Shell script to solve this problem:
$ErrorActionPreference = 'Stop';
Get-ChildItem -Filter '*.rdl' | ForEach {
$reportFile = $_;
Write-Host $reportFile;
$report = [xml](Get-Content $reportFile);
$report.Report.EmbeddedImages.EmbeddedImage | Foreach {
$imagexml = $_;
$imageextension = $imagexml.MIMEType.Split('/')[1];
$filename = $imagexml.Name + '.' + $imageextension;
Write-Host '->' $filename;
$imageContent = [System.Convert]::FromBase64String($imagexml.ImageData);
Set-Content -Path $filename -Encoding Byte -Value $imageContent;
}
}
https://gist.github.com/Fabian-Schmidt/71746e8e1dbdf9db9278
This script extracts all images from all reports in the current folder.
Open the XML (in notepad++ or anything)
Look for the <ImageData></ImageData> tags
Copy the 64-bit encoded string between the tags
Find a utility to convert x64 encoded strings to files. I used this website and downloaded the image
I just needed to do this and realised that it is possible to cut and paste the embedded image, even though it is not possible to copy and paste.

Create IShellLibrary instance in PowerShell

How do you create an IShellLibrary (http://msdn.microsoft.com/en-us/library/windows/desktop/dd391719(v=vs.85).aspx) instance in PowerShell? The IShellLibrary interface makes it possible to manage Windows 7 libraries from code.
Does the Shell.Application COM object implement the IShellLibrary interface? Is it possible to cast a shell object to an IShellLibraryinstance in PowerShell? Below is an example on how to create a Shell.Application instance in PowerShell:
New-Object -ComObject Shell.Application
When I try to cast the object the result is:
PS C:\Users\knut> [IShellLibrary](New-Object -ComObject Shell.Application)
Unable to find type [IShellLibrary]: make sure that the assembly containing this type is loaded.
At line:1 char:16
+ [IShellLibrary] <<<< (New-Object -ComObject Shell.Application)
+ CategoryInfo : InvalidOperation: (IShellLibrary:String) [], RuntimeException
+ FullyQualifiedErrorId : TypeNotFound
Where is the IShellLibrary interface located? How can I import it into the PowerShell session?
You get hold of an IShellLibrary by calling SHCreateLibrary, which creates an empty one. If you want the IShellLibrary for an existing folder, you need SHLoadLibraryFromParsingName or one of the other related functions. They're listed at the bottom of the IShellLibrary page.
It looks like you'll need to use P/Invoke to call them from PowerShell.
Update
http://www.leeholmes.com/blog/2009/01/19/powershell-pinvoke-walkthrough/

Resources