Use GetElementsByClassName in a script - powershell

I'm trying to write a PowerShell script to get the text within all the classes named "newstitle" from a website.
This is what I have:
function check-krpano {
$geturl=Invoke-WebRequest http://krpano.com/news/
$news=$geturl.parsedhtml.body.GetElementsByClassName("newstitle")[0]
Write-Host "$news"
}
check-krpano
It obviously needs much more tweaking, but so far, it doesn't work.
I managed to write an script using GetElementById, but I don't know the syntax for GetElementsByClassName, and to be honest, I haven't been able to find much information about it.
NOTE:
I've ticked the right answer to my question, but that's not the solution that I had chose to use in my script.
Although I was able to find the content within a tag containing a certain class, using 2 methods, they were much slower that searching for links.
Here is the output using Measure-Command:
Search for divs containing class 'newstitle' using parsedhtml.body -> 29.6 seconds
Search for devs containing class 'newstitle' using Allelements -> 10.4 seconds
Search for links which its element 'href' contains #news -> 2.4 seconds
So I have marked as useful the Links method answer.
This is my final script:
function check-krpano {
Clear-Host
$geturl=Invoke-WebRequest http://krpano.com/news
$news = ($geturl.Links |Where href -match '\#news\d+' | where class -NotMatch 'moreinfo+' )
$news.outertext | Select-Object -First 5
}
check-krpano

If you figure out how to get GetElementsByClassName to work, I'd like to know. I just ran into this yesterday and ran out of time so I came up with a workaround:
$geturl.ParsedHtml.body.getElementsByTagName('div') |
Where {$_.getAttributeNode('class').Value -eq 'newstitle'}

getElementsByClassName does not return an array directly but instead a proxy to the results via COM. As you have discovered, conversion to an array is not automatic with the [] operator. You can use the list evaluation syntax, #(), to force it to an array first so that you can access individual elements:
#($body.getElementsByClassName("foo"))[0].innerText
As an aside, conversion is performed automatically if you use the object pipeline, e.g.:
$body.getElementsByClassName("foo") | Select-Object -First 1
It is also performed automatically with the foreach construct:
foreach ($element in $body.getElementsByClassName("foo"))
{
$element.innerText
}

Cannot, for the life of me, get that method to work either!
Depending upon what you need back in the result though, this might help;
function check-krpano {
$geturl=Invoke-WebRequest http://krpano.com/news
$news=($geturl.Links|where href -match '\#news\d+')[0]
$news
}
check-krpano
Gives me back:
innerHTML : krpano 1.16.5 released
innerText : krpano 1.16.5 released
outerHTML : krpano 1.16.5 released
outerText : krpano 1.16.5 released
tagName : A
href : #news1165
You can use those properties directly of course, so if you only wanted to know the most recently released version of krpano, this would do it:
function check-krpano {
$geturl=Invoke-WebRequest http://krpano.com/news
$news=($geturl.Links|where href -match '\#news\d+')[0]
$krpano_version = $news.outerText.Split(" ")[1]
Write-Host $krpano_version
}
check-krpano
would return 1.16.5 at time of writing.
Hope that achieves what you wanted, albeit in a different manner.
EDIT:
This is a possibly a little faster than piping through select-object:
function check-krpano {
$geturl=Invoke-WebRequest http://krpano.com/news
($geturl.Links|where href -match '\#news\d+'|where class -notmatch 'moreinfo+')[0..4].outerText
}

I realize this is an old question, but I wanted to add an answer for anyone else who might be trying to achieve the same thing by controlling Internet Explorer using the COM object like such:
$ie = New-Object -com internetexplorer.application
$ie.navigate($url)
while ($ie.Busy -eq $true) { Start-Sleep -Milliseconds 100; }
I normally prefer to use Invoke-WebRequest as the original poster did, but I've found cases where it seemed like I needed a full-fledged IE instance in order to see all of the JavaScript-generated DOM elements even though I would expect parsedhtml.body to include them.
I found that I could do something like this to get a collection of elements by a class name:
$titles = $ie.Document.body.getElementsByClassName('newstitle')
foreach ($storyTitle in $titles) {
Write-Output $storyTitle.innerText
}
I observed the same really slow performance the original poster noted when using PowerShell to search the DOM, but using PowerShell 3.0 and IE11, Measure-Command shows that my collection of classes is found in a 125 KB HTML document in 280 ms.

It seems to work with PowerShell 5.1:
function check-krpano {
$geturl = Invoke-WebRequest -Uri "http://krpano.com/news/"
$news = $geturl.ParsedHtml.body.getElementsByClassName("newstitle")
Write-Host "$($news[0].innerHTML)"
}
check-krpano
Output:
krpano 1.20.6<SPAN class=smallcomment style="FLOAT: right"><A href="https://krpano.co
m/forum/wbb/index.php?page=Thread&postID=81651#post81651"><IMG class=icon16m src="../design/ico-forumlink
.png"> krpano Forum Link</A></SPAN>

Related

Powershell: Use variable from inner loop as condition of outer loop

we're using a PS-Script for creating new AD-users and configure everything in one go.
I'd like to create a unique 6-digit number and check if it's already in use.
I thought a do-until-loop will do the trick, but i can't get it to re-run if a match is found.
My code as of now is this:
$AllUsedPins=(Get-ADUser -Properties ExtensionAttribute3 -Filter {ExtensionAttribute3 -like '*'} | select -ExpandProperty ExtensionAttribute3)
do{
$PinIsUnique="True"
#$TempPin="333860"
$TempPin=(Get-Random -Minimum 0 -Maximum 999999).ToString('000000')
foreach ($UsedPin in $AllUsedPins) {
If ("$TempPin" -eq "$UsedPin" ) {
$PinIsUnique="False"
Write-Host '$PinIsUnique(IF): '$PinIsUnique
}
}
Write-Host '$PinIsUnique(inDO): '$PinIsUnique
}
until ($PinIsUnique="True")
Write-Host '$PinIsUnique(outDO): '$PinIsUnique
#Set-ADUser -Identity $sam -Replace #{extensionAttribute3=$TempPin}
$TempPin="333860" is for testing, as it is one already used. The loop should then run endlessly, but stops after one run nevertheless, so I added some Write-Outputs to narrow it down. They show:
$PinIsUnique(IF): False
$PinIsUnique(inDO): False
$PinIsUnique(outDO): True
$TempPin: 333860
What puzzles me here:
$PinIsUnique is only set at the beginning of the do-loop, before that it doesn't even exist.
$PinIsUnique is False until end of the do-loop, but True when used for checking the while-condition?
I wasted by far too much time on this already: Reading up on loops in general, nested loops, global variables, return, Write-Host vs Write-Output and other things that sounded as they could help me solve this...
I altered the code for easier testing as nearly half of all options should trigger at least one re-run:
$AllUsedPins=("01", "02", "03", "04", "05", "11", "12", "13", "14", "15")
do{
$PinIsUnique="True"
$TempPin=(Get-Random -Minimum 0 -Maximum 20).ToString('00')
foreach ($UsedPin in $AllUsedPins) {
If ("$TempPin" -eq "$UsedPin" ) {
$PinIsUnique="False"
}
}
}
until ($PinIsUnique="True")
Write-Host '$AllUsedPins: '$AllUsedPins
Write-Host '$TempPin: '$TempPin
As i realized just now while trying the altered code, $TempPin is useable outside the do-loop just fine... So what am I missing here?
Some litte extra if someone cares:
I tried to stop the inner foreach-loop if an existing pin was found, but couldn't get that to work either. Neither break nor a labeled break worked... at least the way i tried.
Also, as i'm no expert in scripting/programming but can usually identify a well-written piece of code: How would you guys have solved this?
Thanks in advance!
Sometimes you just have to talk about a problem to get to the solution... and to be ashamed of yourself...
Just edited
until ($PinIsUnique="True")
to
until ($PinIsUnique -eq "True")
and it works just fine...
Other examples of how to solve it are encouraged, while i'm standing in the corner facing the wall...

How to bind an array to an HTML tag in powershell?

Below is a snip of my powershell code where my response or my variable($witrevisions) is of type array. I am looking to bind this in a html tag which i have defined in the power shell. As I am very new to coding stuff , I am looking the ways how can I bind array to html tag in best possible way
...continuing my line of code
$response4s = (Invoke-RestMethod -Uri $uriAccount -Method get -Headers $AzureDevOpsAuthenicationHeader).values
$wits = $response4s | where({$_.fields.'System.WorkItemType' -eq 'Task'}) # Only retrieve Tasks
$witrevisions = #()
foreach($wit in $response4s){
$customObject = new-object PSObject -property #{
"Title" = $wit.fields.'System.Title'
"AssignedTo" = $wit.fields.'System.AssignedTo'
}
$witrevisions += $customObject
}
$witrevisions | Select-Object `
Title,
AssignedTo
}
and this the sample response i am getting in $witrevisions which i have exported in text file. its a table with two column one having emails and other having a title name.i have tried to show by giving it a table view for better understanding
Assigned To Title
xyz#outlook.com testingpro
drdr#outlook.com resttesting
and here is the html tag where I trying to bind the $witrevisions.
$DOWNLOAD_PAGE_BODY_CONTENT = "<!DOCTYPE html>
`n<html>
`n<head>
`n <title>Validation</title>
`n</head>
`n<body>
`n
`n<p>Please click the link to download the release.</p>
`n<p></p>
`n<p></p>
`n<p>$witrevisions</p>
`n</body>
`n</html>
`n"
Can someone please tell me how should I do this??
Here is an example of some code that would take your array and emit a table, with an explanation to help you tweak to your specific needs:
"<table><body>$($witrevisions|% {"<tr><td>$($_.Title)</td><td>$($_.AssignedTo)</td></tr>"} )</body></table>"
The double quotes are important because they allow string interpolation (it will replace variables with this value, versus being read a plain text. E.g. '[' + $test + ']' => "[$test]"
If you need to do more complex logic in string interpolation, you can use $(...), the ellipses being regular code.
You can iterate through an array by piping to the ForEach-Object, or it's alias %. All the code in the braces will be executed for each item in the array. The current items is represented by $_.
We're then back to string interpolation and using $(...), which is needed to access the members of the current item.
Note: There are several other ways to accomplish (functionally) the same thing. E.g. foreach(...){} vs |%{...}, so feel free to use a different technique if you are more comfortable with doing something a different way.

Is there a "function-like-thing" that can act as a template?

I have a code like this that is repeated multiple times in each of my conditional statements/cases. i have 3 conditions...for now, and everything works perfectly, but im mulling reformatting the script for easier reading.
One of the ways ive thought is to make a function, but the problem is that, i have a while loop that is intended for a specific scenario in each conditional statement that dequeues from a Queue containing some column names from a file.
so based on the code below that i want to put in some sort of template, i cant think of how this could work because as you can see, $tb stands for $table, which is what im opening prior to the conditional statements in my code.
if i were to include everything regarding the server connection and table in a function, that means when i pass the "function" containing the code to the while loops, it will be creating/instantiating the table every iteration, which wont make sense and wont work anyways.
so i am thinking of using something like annotations, something like a template which wont expect to return anything or need reasonable arguments like a function otherwise would. The question is, does something like that exist?
This is the code that is the same across all my while loops that i would like to "store" somewhere and just pass it to them:
$dqHeader = $csvFileHeadersQueue.Dequeue()
$column = New-Object Microsoft.SqlServer.Management.Smo.Column($tb, $dqHeader, $DataType1)
if ($dqHeader -in $PrimaryKeys)
{
# We require a primary key.
$column.Nullable = $false
#$column.Identity = $true #not needed with VarChar
#$column.IdentitySeed = 1 #not needed with VarChar
$tb.Columns.Add($column)
$primaryKey = New-Object Microsoft.SqlServer.Management.Smo.Index($tb, "PK_$csvFileBaseName")
$primaryKey.IndexType = [Microsoft.SqlServer.Management.Smo.IndexType]::ClusteredIndex
$primaryKey.IndexKeyType = [Microsoft.SqlServer.Management.Smo.IndexKeyType]::DriPrimaryKey #Referential Integrity to prevent data inconsistency. Changes in primary keys must be updated in foreign keys.
$primaryKey.IndexedColumns.Add((New-Object Microsoft.SqlServer.Management.Smo.IndexedColumn($primaryKey, $dqHeader)))
$tb.Indexes.Add($primaryKey)
}
else
{
$tb.Columns.Add($column)
}
think of it like a puzzle piece that would fit right in when requested to do so in the while loops to complete that "puzzle"
As per comment:
you can share a (hardcoded) [ScriptBlock] ($template = {code in post goes here}) with a While loop (or function) and invoke it with e.g. Invoke-Command $template or the call operator: &$template. Dynamically modifying an expression and using commands like Invoke-Expression or [ScriptBlock]::Create() is not a good idea due to risk of malicious code injections (see: #1454).
You might even add parameters to your shared [ScriptBlock], like:
$Template = {
[CmdletBinding()]Param ($DataType)
$column = New-Object Microsoft.SqlServer.Management.Smo.Column($tb, $dqHeader, $DataType)
...
}
ForEach ($MyDataType in #('MyDataType')) {
Invoke-Command $Template -ArgumentList $MyDataType
}
But the counter-question remains: Why not just creating a "helper" function?:
Function template($DataType) {
$column = New-Object Microsoft.SqlServer.Management.Smo.Column($tb, $dqHeader, $DataType)
...
}
ForEach ($MyDataType in #('MyDataType')) {
template $MyDataType
}

Get the "plain" (end-user readable) name of UWP apps installed on a system

I'm using the following PowerShell script to retrieve and save to a text file the list of UWP apps on a system. It gets the ID, name (system name) and packagefamilyname.
In addition to the name, I'm looking for a way to retrieve the plain name of the app: for example, "OneNote" instead of "Microsoft.Office.OneNote". Ideally, this name would also be localized: for example, "Calculatrice" (on a French system) instead of "Microsoft.WindowsCalculator".
I found this list of info retrieved by Get-AppxPackage but nothing like an end-user readable name... I'm not very familiar this area of expertise. Any help would be appreciated.
$installedapps = get-AppxPackage
$ids = $null
foreach ($app in $installedapps)
{
try
{
$ids = (Get-AppxPackageManifest $app -erroraction Stop).package.applications.application.id
}
catch
{
Write-Output "No Id's found for $($app.name)"
}
foreach ($id in $ids)
{
$line = $app.Name + "`t" + $app.packagefamilyname + "!" + $id
echo $line
$line >> 'c:\temp\output.txt'
}
}
write-host "Press any key to continue..."
[void][System.Console]::ReadKey($true)
[Completed updated]
You can do this pretty easily in C#. You have to reference the correct WinMDs from the Windows SDK (the actual directories will change depending on SDK version):
C:\Program Files (x86)\Windows Kits\10\References\10.0.17134.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd
C:\Program Files (x86)\Windows Kits\10\References\10.0.17134.0\Windows.Foundation.UniversalApiContract\6.0.0.0\Windows.Foundation.UniversalApiContract.winmd
If you can't build a stand-alone EXE and just want pure PowerShell, you might be able to reference the WinMDs %systemroot%\system32\winmetadata. The code is pretty simple (I avoided await since I don't know if PowerShell has that):
// using Windows.Management.Deployment;
static void Main(string[] args)
{
GetList();
}
static void GetList()
{
var pm = new PackageManager();
var packages = pm.FindPackagesForUser("");
foreach (var package in packages)
{
var asyncResult = package.GetAppListEntriesAsync();
while (asyncResult.Status != Windows.Foundation.AsyncStatus.Completed)
{
Thread.Sleep(10);
}
foreach (var app in asyncResult.GetResults())
{
Console.WriteLine(app.DisplayInfo.DisplayName);
}
}
}
I spent some time looking for this today, and finally came up with a solution that doesn't involve a page of code, or digging around in files/registry/etc. Put the below two lines in a script or function, and it will return PoSh-friendly output which you can then pipe into ForEach-Object, Where-Object, Sort-Object, Export-CSV, etc.
$PkgMgr = [Windows.Management.Deployment.PackageManager,Windows.Web,ContentType=WindowsRuntime]::new()
$PkgMgr.FindPackages() | Select-Object DisplayName -ExpandProperty Id
The .FindPackages() method also has an overload which takes a Family Name, but the docs lead me to believe it can only accept exact names, not wildcard matches. So unless you know exactly what you are looking for, I am guessing it is best to retrieve the list of all packages, and then do your own searches on that list.
The docs do say that this will return packages for all users, and that it requires admin/elevated rights to run.

PowerShell Switch not working with hyphenation

I'm creating a series of new PSObjects, from a CSV import, and then adding them to $new. I'm using a switch to try and set the value for the "Notes" property, as the object is being created\added, and I've run into something 'hinky'.
When I run this...
$import = Import-Csv c:\somerandom.csv
$new = #()
foreach ($Item in $Import) {
$obj = New-Object PsObject -Property #{
Name = $item.Name
Description = $Item.Description
Quantity = $Item.Quantity
Vendor = $Item.Vendor
SubCategory = "Misc"
Notes = ""
}
switch ($obj.Name) {
"iPod" { $obj.Notes = "Burn with the rest of the Apple garbage"}
"nVidia GTX 780ti" { $obj.Notes = "Steal immediately!" }
default { $obj.Notes= "Sorry man... I have no idea what that is"}
}
$new += $obj
}
... it works as expected. All of the entries from $import, are recreated in $new, with the addition of my "SubCategory" and "Notes" noteproperties (iPod gets burn tag, 780ti slated to be stolen). But when I run with the following as the switch...
switch ($obj.Name) {
'SOFM090-107-01-PF-R' { $obj.Notes = "Burn with the rest of the Apple garbage"}
'M094-107-01-PF-R' { $obj.Notes = "Steal immediately!" }
default { $obj.Notes = "Sorry man... I have no idea what that is"}
}
... It sets all the entries to the 'default' setting on the switch. I tried running the switch with a non-hyphenated name for one entry, and a hyphenated entry for the other, and only the hyphenated version was set properly.
The above code is altered from the actual code, but it properly illustrates what I'm trying to do. I need to add a noteproperty that is based off a list of part numbers, and will fill in the "Notes" entry with a tag of my choosing.
I've tried it with single quotes, double quotes, using the -wildcard and replacing the switch hyphens with *'s, and putting the ` character in before the -'s. Nothing seems to be working.
There's nothing wrong with the code, so the problem must lie in the data. I verified that it works fine with a CSV file that has those exact hyphenated values in the "Name" column.
If the switch doesn't work with the hyphenated names, then the values being imported into the Name property don't match what you have in the switch statement. It's a good idea to always post the data you're working with, or a sample of it, because often that's the source of the problem. Even when it isn't, it helps other people understand what you're trying to accomplish and what your code does. Since we don't have the data, I can suggest a few likely possibilities:
You're manually typing the names into the switch statement, and they look like what's in the CSV, but don't actually match, e.g. you're confusing O with 0 because they look the same in the font you're working with. I'd have suspected something like an en-dash instead of a hyphen, but you say you tried replacing the hyphens with wildcards
You have trailing spaces
You're single-quoting the hyphenated names in the CSV file (Import-Csv only understands double quotes; single quotes would be included in the value).
Here are a couple of things you can try to help identify why the data doesn't match (separately, not both together):
Replace switch ($obj.Name) { with switch -regex ($obj.Name) {
Use the following code to show you exactly what PowerShell is seeing in the Name property for each item and which switch conditions are being executed:
Write-Host -NoNewline "[$($obj.Name)] "
switch ($obj.Name) {
'SOFM090-107-01-PF-R' {Write-Host 'burn'; $obj.Notes = "Burn with the rest of the Apple garbage"}
'M094-107-01-PF-R' {Write-Host 'steal'; $obj.Notes = "Steal immediately!"}
default {Write-Host 'sorry'; $obj.Notes = "Sorry man... I have no idea what that is"}
}
If you post the data, we'll probably be able to tell you exactly why it's not working. But I can pretty much guarantee you that if you're using that code, the problem is that the imported Name values that aren't being matched with the right Notes values are in some way not the same as what you have in the switch conditions.