ConfluenceStuff.psm1
function Compare-ConfluencePageTable { <# .SYNOPSIS Function for comparing two list of objects. First one is gathered from given Confluence wiki page table (identified using page ID and table index), second one is given by parameter newContent. If both are the same, return $true else $false. Can be used for detection whether confluence page table has to be filled with new data. .DESCRIPTION Function for comparing two list of objects. First one is gathered from given Confluence wiki page table (identified using page ID and table index), second one is given by parameter newContent. If both are the same, return $true else $false. Can be used for detection whether confluence page table has to be filled with new data. Text values are trimmed before compare operation. .PARAMETER newContent Object(s) that will be compared with content gathered from Confluence page table. .PARAMETER pageID ID of the Confluence page where table content for compare will be gathered. .PARAMETER property Optional parameter for specifying list of properties, that should be used for compare. Otherwise all available properties will be used. .PARAMETER excludeProperty Optional parameter for specifying list of properties, that should be excluded from compare. .PARAMETER index Index of the table to get the content from. By default 0 a.k.a. the first one. .EXAMPLE Connect-Confluence $ADComputer = Get-ADComputer -filter * -properties name, description, DistinguishedName Compare-ConfluencePageTable -newContent $ADComputer -pageID "1318781218" -property name,description Will return $true if content of $ADComputer is the same as content of first HTML table on the Confluence page with ID "1318781218". For comparison of objects only name and description properties will be used. .OUTPUTS Boolean. True if content matches otherwise False. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $newContent , [Parameter(Mandatory = $true)] [Uint64] $pageID , [string[]] $property , [string[]] $excludeProperty , [int] $index = 0 ) $confluenceContent = Get-ConfluencePageTable -pageID $pageID -index $index $params = @{ input1 = $newContent input2 = $confluenceContent trimStringProperty = $true #outputItemWithoutMatch = $true } if ($property) { $params.property = $property } if ($excludeProperty) { $params.excludeProperty = $excludeProperty } if ($VerbosePreference -eq "Continue") { $params.Verbose = $true } # compare confluence page content with the given one Compare-Object2 @params } function Connect-Confluence { <# .SYNOPSIS Function for connecting to Confluence a.k.a. setting default ApiUri and Credential parameters for every Confluence cmdlet. .DESCRIPTION Function for connecting to Confluence a.k.a. setting default ApiUri and Credential parameters for every Confluence cmdlet. Detects already existing connection and validates provided credentials. .PARAMETER baseUri Base URI of your cloud Confluence page. It should look like 'https://contoso.atlassian.net/wiki'. .PARAMETER credential Credentials for connecting to your cloud Confluence API. Use login and generated PAT (not password!). .PARAMETER pageSize The default page size for all commands is 25. Using the -PageSize parameter changes the default for all commands in your current session. .EXAMPLE Connect-Confluence -baseUri 'https://contoso.atlassian.net/wiki' -credential (Get-Credential) Connects to 'https://contoso.atlassian.net/wiki' cloud Confluence base page using provided credentials. .NOTES Has to be used instead of the official Set-ConfluenceInfo because of scoping problem when setting PSDefaultParameterValues! #> [CmdletBinding()] param ( [ValidateScript( { if ($_ -match "^https://.+/wiki$") { $true } else { throw "$_ is not a valid Confluence wiki URL. Should be something like 'https://contoso.atlassian.net/wiki'" } })] [string] $baseUri = $_baseUri, [System.Management.Automation.PSCredential] $credential, [UInt32] $pageSize ) if (!$baseUri) { throw "BaseUri parameter has to be set. Something like 'https://contoso.atlassian.net/wiki'" } #region helper functions # this function originates from the official ConfluencePS module # it needs to be call from inside my module so the PSDefaultParameterValues default parameters are set in the correct scope function Set-Info { [CmdletBinding()] param ( [Parameter( HelpMessage = 'Example = https://brianbunke.atlassian.net/wiki (/wiki for Cloud instances)' )] [uri]$BaseURi, [PSCredential]$Credential, [UInt32]$PageSize, [switch]$PromptCredentials ) BEGIN { function Add-ConfluenceDefaultParameter { param( [Parameter(Mandatory = $true)] [string]$Command, [Parameter(Mandatory = $true)] [string]$Parameter, [Parameter(Mandatory = $true)] $Value ) PROCESS { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Setting [$command : $parameter] = $value" # Needs to set both global and module scope for the private functions: # http://stackoverflow.com/questions/30427110/set-psdefaultparametersvalues-for-use-within-module-scope $PSDefaultParameterValues["${command}:${parameter}"] = $Value $global:PSDefaultParameterValues["${command}:${parameter}"] = $Value } } $moduleCommands = Get-Command -Module 'ConfluencePS' if ($PromptCredentials) { $Credential = (Get-Credential) } } PROCESS { foreach ($command in $moduleCommands) { $parameter = "ApiUri" if ($BaseURi -and ($command.Parameters.Keys -contains $parameter)) { Add-ConfluenceDefaultParameter -Command $command.name -Parameter $parameter -Value ($BaseURi.AbsoluteUri.TrimEnd('/') + '/rest/api') } $parameter = "Credential" if ($Credential -and ($command.Parameters.Keys -contains $parameter)) { Add-ConfluenceDefaultParameter -Command $command.name -Parameter $parameter -Value $Credential } $parameter = "PageSize" if ($PageSize -and ($command.Parameters.Keys -contains $parameter)) { Add-ConfluenceDefaultParameter -Command $command.name -Parameter $parameter -Value $PageSize } } } } #endregion helper functions # check whether already connected $setApiUri = $PSDefaultParameterValues.GetEnumerator() | ? Name -EQ "Get-ConfluencePage:ApiUri" | select -ExpandProperty Value # authenticate to Confluence if ($setApiUri -and $setApiUri -like "$baseUri*") { Write-Verbose "Already connected to $baseUri" # I assume that provided credentials are correct return } else { Write-Verbose "Setting ApiUri and Credential parameters for every Confluence cmdlet a.k.a. connecting to Confluence" Add-Type -AssemblyName System.Web while (!$credential) { $credential = Get-Credential -Message "Enter login and API key (instead of password!) for connecting to the Confluence" } # check whether provided credentials are valid [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # create basic auth. header $Headers = @{"Authorization" = "Basic " + [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(($credential.UserName + ":" + [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($credential.Password)) ))) } try { $null = Invoke-WebRequest -Method GET -Headers $Headers -Uri "$baseUri/rest/api/content" -UseBasicParsing -ErrorAction Stop } catch { if ($_ -like "*(401) Unauthorized*") { throw "Provided Confluence credentials aren't valid (have you provided PAT instead of password?). Error was: $_" } else { throw $_ } } # set default Confluence command parameters (ApiUri, Credential,..) $param = @{ BaseURi = $baseUri Credential = $credential } if ($pageSize) { $param.PageSize = $pageSize } Set-Info @param } } function ConvertTo-ConfluenceTableHtml { <# .SYNOPSIS Function converts given object into HTML table code. Should be used instead of original '$someObject | ConvertTo-ConfluenceTable | ConvertTo-ConfluenceStorageFormat', because: - pipe '|' sign in object value no more breaks table formatting - values in cells are not surrounded with spaces a.k.a. table columns can be sorted .DESCRIPTION Function converts given object into HTML table code. Should be used instead of original '$someObject | ConvertTo-ConfluenceTable | ConvertTo-ConfluenceStorageFormat', because: - pipe '|' sign in object value no more breaks table formatting - values in cells are not surrounded with spaces a.k.a. table columns can be sorted You have to be authenticated to the Confluence before you use this function! .PARAMETER object PowerShell object that should be converted into the HTML table. .EXAMPLE # connect to your Confluence wiki Connect-Confluence # convert given objects to HTML table $tableHtml = ConvertTo-ConfluenceTableHtml -object (get-process svchost | select name, cpu, id ) # replace existing Confluence page content with your table Set-ConfluencePage -pageID 1234 -body $tableHtml .NOTES Requires original ConfluencePS module. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $object ) # requirements check if (!(Get-Module ConfluencePS) -and !(Get-Module ConfluencePS -ListAvailable)) { throw "Module ConfluencePS is missing. Function $($MyInvocation.MyCommand) cannot continue" } #region replace '|' a.k.a. pipe sign temporarily, because confluence ConvertTo-ConfluenceTable uses it for describing html table form $pipePlaceholder = "###PIPE###" $object = $object | % { $obj = $_ $obj | Get-Member -MemberType NoteProperty, Property, Properties, ScriptProperty | select -ExpandProperty name | % { if ($obj.$_ -match "\|") { Write-Verbose "replacing '|' in: $($obj.$_)" $obj.$_ = $obj.$_ -replace "\|", $pipePlaceholder } } $obj } #endregion replace '|' a.k.a. pipe sign temporarily, because confluence ConvertTo-ConfluenceTable uses it for describing html table form $confluenceTableFormat = $object | ConvertTo-ConfluenceTable -ErrorAction Stop | ConvertTo-ConfluenceStorageFormat # replace pipe placeholder back to pipe sign $confluenceTableFormat = $confluenceTableFormat -replace $pipePlaceholder, "|" #region get rid of surrounding white spaces to make the table sortable <# <th><p> Name </p></th> <th><p> Id </p></th> <th><p> CPU </p></th> converts to: <th><p>Name</p></th> <th><p>Id</p></th> <th><p>CPU</p></th> #> $confluenceTableFormat = $confluenceTableFormat -replace "><p>\s+", "><p>" -replace "\s+</p><", "</p><" $confluenceTableFormat #endregion get rid of surrounding white spaces to make the table sortable } function Get-ConfluencePage2 { <# .SYNOPSIS Function returns Confluence page content using native Invoke-WebRequest. Returned object contains parsed HTML (as Com object), raw HTML page content etc. .DESCRIPTION Function returns Confluence page content using native Invoke-WebRequest. Returned object contains parsed HTML (as Com object), raw HTML page content etc. .PARAMETER pageID ID of the Confluence page. Can be extracted from page URL https://contoso.atlassian.net/wiki/spaces/KID/pages/123456789/dummyname a.k.a. it's 123456789 in this case. .PARAMETER header Authentication header created using Create-BasicAuthHeader. .EXAMPLE $baseUri = 'https://contoso.atlassian.net/wiki' $credential = Get-Credential Connect-Confluence -baseUri $baseUri -credential $credential $header = Create-BasicAuthHeader -credential $credential $response = Get-ConfluencePage2 -pageId 123456789 -baseUri $baseUri -header $header # get page html code as a string $response.Content # get page as parsed Com object $response.ParsedHtml # use parsed Com object for extracting existing table as a psobject ConvertFrom-HTMLTable -htmlComObj $response.ParsedHtml #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Uint64] $pageID, [Parameter(Mandatory = $true)] [ValidateScript( { if ($_ -match "^https://.+/wiki$") { $true } else { throw "$_ is not a valid Confluence wiki URL. Should be something like 'https://contoso.atlassian.net/wiki'" } })] [string] $baseUri, [Parameter(Mandatory = $true)] $header ) [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 try { $response = Invoke-WebRequest -Method GET -Headers $header -Uri "$baseUri/rest/api/content/$pageID`?expand=body.storage" -ea stop } catch { if ($_.exception -match "The response content cannot be parsed because the Internet Explorer engine is not available") { throw "Error was: $($_.exception)`n Run following command on $env:COMPUTERNAME to solve this:`nSet-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main' -Name DisableFirstRunCustomize -Value 2" } else { throw $_ } } $response.ParsedHtml } function Get-ConfluencePageTable { <# .SYNOPSIS Function extracts table from given Confluence page and converts it into the psobject. .DESCRIPTION Function extracts table from given Confluence page and converts it into the psobject. .PARAMETER pageID Confluence page ID. .PARAMETER index Index of the table to extract. By default 0 a.k.a. the first one. .PARAMETER useHTMLAgilityPack Switch for using 3rd party HTML Agility Pack dll (requires PowerHTML wrapper module!) instead of the native one. Mandatory for Core OS, Azure Automation etc, where native dll isn't available. Also it is much faster then native parser which sometimes is suuuuuuper slow. .PARAMETER splitValue Switch for splitting table cell values a.k.a. get array of cell values instead of one string. Delimiter is defined in splitValueBy parameter. .PARAMETER splitValueBy Delimiter for splitting column values. .PARAMETER all Switch to process all tables in given HTML. .PARAMETER tableName Adds property tableName with given name to each returned object. If more than one table is returned, adds table number suffix to the given name. .PARAMETER omitEmptyTable Switch to skip empty tables. Empty means there are no other rows except the header one. .PARAMETER asArrayOfTables Switch for returning the result as array of tables where each array contains rows of such table. By default array of all rows from all tables is being returned at once. Beware that if only one table is returned, PowerShell automatically expands this one array to array of containing items! To avoid this behavior use @(): $result = @(ConvertFrom-HTMLTable -htmlFile "C:\Users\Public\Documents\MDMDiagnostics\MDMDiagReport.html" -all -asArrayOfTables). .EXAMPLE Connect-Confluence Get-ConfluencePageTable -PageID 123456789 Get & convert just first table existing at given confluence page using native parser. Table lines will be returned one by one. .EXAMPLE Connect-Confluence Get-ConfluencePageTable -PageID 123456789 -useHTMLAgilityPack -index 1 Get & convert just second table existing at given confluence page using 3rd party (HTML Agility Pack) parser. Table lines will be returned one by one. .EXAMPLE Connect-Confluence Get-ConfluencePageTable -PageID 123456789 -useHTMLAgilityPack -all -omitEmptyTable -asArrayOfTables Get & convert all tables existing at given confluence page using 3rd party (HTML Agility Pack) parser. Table's lines will be returned inside an array a.k.a. result will be array of arrays. Empty tables will be omitted (instead of returning empty object). #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Uint64] $pageID , [ValidateNotNullOrEmpty()] [int] $index = 0 , [switch] $useHTMLAgilityPack , [switch] $splitValue , [string] $splitValueBy = "," , [switch] $all, [string] $tableName, [switch] $omitEmptyTable, [switch] $asArrayOfTables ) # requirements check if (!(Get-Module ConfluencePS) -and !(Get-Module ConfluencePS -ListAvailable)) { throw "Module ConfluencePS is missing. Function $($MyInvocation.MyCommand) cannot continue" } # get confluence page content $pageContent = (Get-ConfluencePage -PageID $pageID -ea Stop).body # create ConvertFrom-HTMLTable parameter hash $param = @{ htmlString = $pageContent } # pass other defined parameters # exclude parameters not for ConvertFrom-HTMLTable $PSBoundParameters.getenumerator() | ? { $_.key -ne 'pageID' } | % { $param.($_.key) = $_.value } # extract & convert table(s) from given html code using provided parameters ConvertFrom-HTMLTable @param } function Set-ConfluencePage2 { <# .SYNOPSIS Proxy function for Set-ConfluencePage. Adds possibility to set just selected table's content on given page (and leave rest of the page intact). .DESCRIPTION Proxy function for Set-ConfluencePage. Adds possibility to set just selected table's content on given page (and leave rest of the page intact). .PARAMETER pageID Page ID of the Confluence page. .PARAMETER body HTML code that should be set as the new page content. In case you use setJustTable switch, given HTML code will replace just code of the specified (tableIndex) table. .PARAMETER setJustTable Switch for replacing just specified (tableIndex) table's HTML code (body) that is on Confluence page, nothing else. .PARAMETER tableIndex Index of the HTML table you want to replace by code specified in body parameter. Used only when setJustTable parameter is used. By default 0 a.k.a. the first one. .EXAMPLE Connect-Confluence $body = get-process notepad | select name, cpu, id | ConvertTo-ConfluenceTable | ConvertTo-ConfluenceStorageFormat Set-ConfluencePage2 -pageID 1234 -body $body -setJustTable Replace just HTML code of the first table on the Confluence page (ID 1234) with new code (specified in body parameter). Leaves what was before and after that table intact. .EXAMPLE Connect-Confluence $body = get-process notepad | select name, cpu, id | ConvertTo-ConfluenceTable | ConvertTo-ConfluenceStorageFormat Set-ConfluencePage2 -pageID 1234 -body $body -setJustTable -tableIndex 1 Replace just HTML code of the second table on the Confluence page (ID 1234) with new code (specified in body parameter). Leaves what was before and after that table intact. .NOTES Update of existing table was inspired by https://garytown.com/atlassian-confluence-updating-tables-with-powershell. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [Uint64] $pageID, [Parameter(Mandatory = $true)] [string] $body, [switch] $setJustTable, [int] $tableIndex = 0 ) try { $pageToUpdate = Get-ConfluencePage -PageID $pageID -ErrorAction Stop } catch { throw "You have to connect to the Confluence page first. Error was:`n$_" } if ($setJustTable) { # only specified ($tableIndex) html table should be updated using given html code ($body) # rest of the page should stay intact # open table tag because of: <table data-layout="default" and such $tableOpenTagRegex = "<table[^>]*>" $tableCloseTagRegex = "</table>" if ($body -notmatch $tableOpenTagRegex) { throw "Body parameter should contains string defining HTML table because you've used setJustTable switch" } # body is one big one-liner # to make it easy to work with, split it into the lines by closing tags $pageToUpdateBody = ($pageToUpdate.body -replace "><", ">`n<") -split "`n" $tableCount = ($pageToUpdateBody -match $tableOpenTagRegex).count if (!$tableCount) { throw "Confluence page doesn't contain any table to update" } elseif ($tableIndex -gt ($tableCount - 1)) { throw "Confluence page contains $tableCount table(s), but you want to update table number $($tableIndex + 1)" } else { Write-Verbose "Page contains $tableCount table(s)" } #region get & save all open/close html table tag line indexes # TIP: I assume tables are not nested # index of the html code line $i = 0 $tableTagIndex = @() $pageToUpdateBody | % { if (($_ -match $tableOpenTagRegex) -or ($_ -match $tableCloseTagRegex)) { "Line with index: $i contains open/close table tag: $_" $tableTagIndex += $i } ++$i } # check whether number of html table tags is even if ($tableTagIndex.count % 2) { throw "Some opening or closing HTML table tag is missing" } #endregion get & save all open/close html table tag line indexes # create array of arrays where each array contains line indexes of open/close tags of one of the html tables on existing page $tableList = @() for ($i = 0; $i -le ($tableTagIndex.count - 1); ($i = $i + 2)) { $tableList += , @($tableTagIndex[$i], $tableTagIndex[$i + 1]) } # get open/close line indexes of specified table $tableToUpdateIndex = $tableList[$tableIndex] $tableToUpdateOpenTagIndex = $tableToUpdateIndex[0] $tableToUpdateCloseTagIndex = $tableToUpdateIndex[1] Write-Verbose "Table to replace starts at $tableToUpdateOpenTagIndex line index and ends at $tableToUpdateCloseTagIndex" # get html code that is before specified table if ($tableToUpdateOpenTagIndex -eq 0) { # table is first element on the page $bodyBeforeTable = $null } else { # there is some content on the page before the table $bodyBeforeTable = $pageToUpdateBody[0..($tableToUpdateOpenTagIndex - 1)] } # get html code that is after specified table if ($tableToUpdateCloseTagIndex -eq ($pageToUpdateBody.count - 1)) { # table is last element on the page $bodyAfterTable = $null } else { # there is some content on the page after the table $bodyAfterTable = $pageToUpdateBody[($tableToUpdateCloseTagIndex + 1)..(($pageToUpdateBody.count - 1))] } # take existing page html code and replace specified table's code with the new one $body = ($bodyBeforeTable -join '') + $body + ($bodyAfterTable -join '') } Write-Verbose "Set content of the Confluence page with ID $pageID to:`n$body" Set-ConfluencePage -PageID $pageID -Body $body } Export-ModuleMember -function Compare-ConfluencePageTable, Connect-Confluence, ConvertTo-ConfluenceTableHtml, Get-ConfluencePage2, Get-ConfluencePageTable, Set-ConfluencePage2 |