7

I have been running a password expiration script for the pass 6 months without any issue. The script will read in a static html file and change around some of the content in memory and then an html email will be sent to all users who have expiring passwords.

The script seems to have broke in the past week or so. Upon further investigation I've narrowed down the errors to the section where Powershell is supposed to create a new ComObject and write that HTML file to the ComObject.

I now get the error :

No coercion operator is defined between types 'System.Array' and 'System.String'
At line:1 char:1
+ $html.write($source);
+ ~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [], InvalidOperationException
    + FullyQualifiedErrorId : System.InvalidOperationException

The above error happens when I run below lines of code :

$html = New-Object -ComObject "HTMLFile"
$src = Get-Content -path "./passwordreminder.html" -Raw
$html.write($src)

When I invoke the write() method I get the error.

Since its been working fine for the last 6 months, the only thing that I can think of that has changed is my version of powershell. I believe when I started running this script I was using Powershell v4.0, but after Windows Updates I guess Powershell is now at v5.0. See below :

Name                           Value
----                           -----
PSVersion                      5.0.10105.0
WSManStackVersion              3.0
SerializationVersion           1.1.0.1
CLRVersion                     4.0.30319.34209
BuildVersion                   10.0.10105.0
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion      2.3

The script is running on Windows Server 2012 R2 OS.

Anyone have any ideas?

I've seen some suggestions in other questions calling to use the IHTMLDocument2_write() method on the ComObject, but this method doesn't exist when I try to invoke it.

Update :

I was able to confirm that this is INDEED BROKEN in my version of Powershell.

I was just able to test the same code on a different server with the same OS but below version of Powershell :

Name                           Value
----                           -----
PSVersion                      4.0
WSManStackVersion              3.0
SerializationVersion           1.1.0.1
CLRVersion                     4.0.30319.34014
BuildVersion                   6.3.9600.17090
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0}
PSRemotingProtocolVersion      2.2

And the code works as expected.

Anybody know what can be used in this new version of Powershell?

Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
kevin
  • 304
  • 3
  • 4
  • 11
  • 1
    You could try with an IE object: `$ie = New-Object -COM 'InternetExplorer.Application'; $ie.Navigate("file://$($PWD.Path)/passwordreminder.html"`. I don't have PowerShell v5, though, so I can't test. If `HTMLFile` is broken, this might be as well. – Ansgar Wiechers Jun 20 '15 at 18:55
  • 1
    On both the working and non-working servers can you change the offending line to `$html.Write;`, run it, and compare the results? This will output which overloads of the `Write` method are available on the `$html` object. Perhaps a new one is available and PowerShell is now trying to invoke the "wrong" one. – Lance U. Matthews Jun 20 '15 at 19:20
  • @Ansgar Thanks that still works on v5.0!!!! I was able to rewrite part of the script using this method. However, this method is certainly not ideal. The script will edit the HTML document in memory for EACH USER that has an expiring password coming up. This mean I have to spin up a new IE process for everyone. This could be up to 20-30+ processes on some days. I can mitigate this by running a `stop-process` to kill some of them but still the overhead is HUGE compared to the previously working solution. I am going to use this until I can find a better work around. Thanks!!!! – kevin Jun 20 '15 at 20:06
  • @BACON I am not sure I follow what you are telling me to do. Can you please give me the code you would like me to run on both servers? That might help me understand better. The only think I can think you want me to do is pipe `$html` object into `Get-Member` to see if the methods appear to be different. Is that what you mean? – kevin Jun 20 '15 at 20:13
  • @Ansgar Actually I can just re-use the $ie object so I wouldn't have to spin up a new process every time. So that would work out! Sorry was to quick to respond before I though about it more. – kevin Jun 20 '15 at 20:27
  • @kevin BACON wants you to run this: `$html = New-Object -ComObject "HTMLFile"; $html.write` (just `write` without any argument or parentheses). It should output the overloads for the `Write()` method. – Ansgar Wiechers Jun 20 '15 at 21:25

4 Answers4

7

This seems to work properly if you provide a UCS-2 byte array instead of a string:

$html = New-Object -ComObject "HTMLFile"
$src = Get-Content -path "./passwordreminder.html" -Raw
$src = [System.Text.Encoding]::Unicode.GetBytes($src)
try
{
    # This works in PowerShell 4
    $html.IHTMLDocument2_write($src)
}
catch
{
    # This works in PowerShell 5
    $html.write($src)
}
jedigo
  • 901
  • 8
  • 12
  • I tried this but got exception thrown : **Exception calling "write" with "1" argument(s): "Type mismatch.** – kevin Jun 14 '16 at 18:02
  • I have changed this to use my working code (which works on both PowerShell 4 and 5). I am guessing you were not using PowerShell 5? – jedigo Jun 15 '16 at 16:54
2

You could try with an Internet Explorer COM object:

$ie = New-Object -COM 'InternetExplorer.Application'

$ie.Navigate("file://$($PWD.Path)/passwordreminder.html")
do {
  Start-Sleep -Milliseconds 100
} until ($ie.ReadyState -eq 4)

# do stuff

I don't have PowerShell v5, though, so I can't test. If HTMLFile is broken, this might be as well.

You can call the Navigate() method (and the loop waiting for it to complete loading the page) in an outer loop if you need to run it repeatedly.

$ie = New-Object -COM 'InternetExplorer.Application'

foreach (...) {
  $ie.Navigate("file://$($PWD.Path)/passwordreminder.html")
  do {
    Start-Sleep -Milliseconds 100
  } until ($ie.ReadyState -eq 4)

  # do stuff
}
Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
  • **Powershell v4.0 :** MemberType : Method OverloadDefinitions : {void write (SAFEARRAY(Variant)) TypeNameOfValue : System.Management.Automation.PSMethod Value : void write (SAFEARRAY(Variant)) Name : write IsInstance : True **Powershell v5.0 :** MemberType : Method OverloadDefinitions : {void write (SAFEARRAY(Variant))} TypeNameOfValue : System.Management.Automation.PSMethod Value : void write (SAFEARRAY(Variant)) Name : write IsInstance : True – kevin Jun 20 '15 at 22:04
2

This code snippet works by adding the .NET Framework's mshtml.HTMLDocumentClass type via the Add-Type -AssemblyName cmdlet.

Add-Type -AssemblyName "Microsoft.mshtml"
$html = New-Object -ComObject "HTMLFile"
$svc = Get-Service | Select-Object Name, Status | ConvertTo-Html
$svc | Out-File -FilePath .\report.html -Force
$htmlFile = Get-Content -Path .\report.html -Raw
$html.IHTMLDocument2_write($htmlFile)

The $html variable contains the "HTMLFile" object reference with all its methods and properties.

  • Adding the assembly is unnecessary, that does nothing for COM objects. But using `.IHTMLDocument2_write()` instead of `.write()` does the trick. All other attempts have lead to "Type mismatch" errors. – Tomalak May 04 '21 at 11:30
0

Solved by adding path reference and specifying the type of object

Add-Type -Path "C:\Program Files (x86)\Microsoft.NET\Primary Interop Assemblies\Microsoft.mshtml.dll"

$webpage = New-Object mshtml.HTMLDocumentClass

Here is the full code

$url = 'http://website'
$outFile = 'C:\content.txt'
$showCount = 10;

[net.httpwebrequest]$httpwebrequest = [net.webrequest]::create($url)
[net.httpWebResponse]$httpwebresponse = $httpwebrequest.getResponse()
$reader = new-object IO.StreamReader($httpwebresponse.getResponseStream())
$html = $reader.ReadToEnd()
$reader.Close()

Add-Type -Path "C:\Program Files (x86)\Microsoft.NET\Primary Interop Assemblies\Microsoft.mshtml.dll"


$webpage = New-Object mshtml.HTMLDocumentClass
$webpage.IHTMLDocument2_write($html)

$topicElements = $webpage.documentElement.getElementsByClassName('topic')

$time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
$content = '[www.hkgalden.com] [' + $time + '] '

$i = 0;
foreach ($topicElement in $topicElements) {
    $titleElement = $topicElement.getElementsByClassName('title')[0].getElementsByTagName('a')[0]
    $title = $titleElement.innerText

    $usernameElement = $topicElement.getElementsByClassName('username')[0]
    $username = $usernameElement.innerText

    $content += $username + ': ' + $title + ' // '
    $i++
    if ($i -gt $showCount) {
        break
    }
}
#$content
$content | Out-File -Encoding utf8 $outFile