diff --git a/.github/scripts/Expose-TestData.ps1 b/.github/scripts/Expose-TestData.ps1 new file mode 100644 index 0000000..6897c2a --- /dev/null +++ b/.github/scripts/Expose-TestData.ps1 @@ -0,0 +1,138 @@ +if ([string]::IsNullOrWhiteSpace($env:PSMODULE_TEST_DATA)) { + Write-Output 'No test data was provided by the calling workflow.' + return +} +try { + $data = $env:PSMODULE_TEST_DATA | ConvertFrom-Json -ErrorAction Stop +} catch { + throw "The 'TestData' secret must be valid JSON with 'secrets' and/or 'variables' maps." +} +if ($null -eq $data -or $data -isnot [pscustomobject]) { + throw "The 'TestData' secret must be a JSON object with 'secrets' and/or 'variables' maps." +} +$allowedTopLevelKeys = @('secrets', 'variables') +foreach ($propertyName in $data.PSObject.Properties.Name) { + if ($allowedTopLevelKeys -notcontains $propertyName) { + throw "The 'TestData' secret only supports 'secrets' and 'variables' maps." + } +} +$reservedNames = @('CI', 'HOME', 'PATH', 'PWD', 'SHELL', 'PSMODULE_TEST_DATA') +$reservedPrefixes = @('GITHUB_', 'RUNNER_', 'ACTIONS_') +function Assert-EnvironmentName { + <# + .SYNOPSIS + Validates that a TestData key can safely be written to GITHUB_ENV. + #> + param([string] $Name) + if ($Name -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { + throw 'TestData keys must be valid environment variable names.' + } + $normalized = $Name.ToUpperInvariant() + if ($reservedNames -contains $normalized) { + throw 'TestData keys must not override reserved environment variables.' + } + foreach ($prefix in $reservedPrefixes) { + if ($normalized.StartsWith($prefix)) { + throw 'TestData keys must not override reserved environment variables.' + } + } +} +function Assert-Map { + <# + .SYNOPSIS + Validates that a TestData section is a JSON object map. + #> + param( + [object] $Map, + [string] $Name + ) + if ($null -eq $Map) { return } + if ($Map -isnot [pscustomobject]) { + throw "The 'TestData.$Name' value must be a JSON object." + } +} +function Get-EnvironmentValue { + <# + .SYNOPSIS + Converts a scalar TestData value to an environment variable value. + #> + param( + [object] $Value, + [string] $Name + ) + if ($null -eq $Value) { return '' } + if ( + $Value -is [pscustomobject] -or + ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) + ) { + throw "Values in 'TestData.$Name' must be scalar values." + } + return [string]$Value +} +function Add-EnvFromMap { + <# + .SYNOPSIS + Writes validated TestData entries to GITHUB_ENV. + #> + param( + [object] $Map, + [string] $Name, + [switch] $Mask + ) + Assert-Map -Map $Map -Name $Name + if ($null -eq $Map) { return } + $count = 0 + foreach ($item in $Map.PSObject.Properties) { + $name = $item.Name + Assert-EnvironmentName -Name $name + $value = Get-EnvironmentValue -Value $item.Value -Name $Name + if ($Mask) { + foreach ($line in ($value -split "`n")) { + $line = $line.TrimEnd("`r") + if ($line.Length -gt 0) { + Write-Output "::add-mask::$line" + } + } + } + do { + $delimiter = "GHENV_$([guid]::NewGuid().ToString('N'))" + } while ($value.Contains($delimiter)) + Add-Content -Path $env:GITHUB_ENV -Value "$name<<$delimiter" -Encoding utf8 + Add-Content -Path $env:GITHUB_ENV -Value $value -Encoding utf8 + Add-Content -Path $env:GITHUB_ENV -Value $delimiter -Encoding utf8 + $count++ + } + if ($count -gt 0) { + if ($Mask) { + Write-Output "Exposed $count secret value(s) as environment variables." + } else { + Write-Output "Exposed $count variable value(s) as environment variables." + } + } +} + +Assert-Map -Map $data.secrets -Name 'secrets' +Assert-Map -Map $data.variables -Name 'variables' + +$secretNames = @() +if ($null -ne $data.secrets) { + $secretNames = @($data.secrets.PSObject.Properties.Name) +} +$variableNames = @() +if ($null -ne $data.variables) { + $variableNames = @($data.variables.PSObject.Properties.Name) +} +$secretNameSet = [System.Collections.Generic.HashSet[string]]::new( + [System.StringComparer]::OrdinalIgnoreCase +) +foreach ($secretName in $secretNames) { + [void] $secretNameSet.Add($secretName) +} +foreach ($variableName in $variableNames) { + if ($secretNameSet.Contains($variableName)) { + throw 'TestData keys must not be duplicated across secrets and variables.' + } +} + +Add-EnvFromMap -Map $data.secrets -Name 'secrets' -Mask +Add-EnvFromMap -Map $data.variables -Name 'variables' diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index 93c1ddd..e8c9547 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -27,5 +27,19 @@ permissions: jobs: Process-PSModule: - uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@ce64918acc96dda73eb78f827036b794bfa6fa1a # v5.5.7 - secrets: inherit + # The TestData secret (PR #365) shipped in v6.0.0; pinned to the current release v6.1.1. + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@ea19a3eb1293246d1b00e4faae1544442c3be0ec # v6.1.1 + secrets: + # PowerShell Gallery API key used to publish the module. + APIKey: ${{ secrets.APIKey }} + # All data the module's tests need, in one object: a "secrets" map (masked) and a "variables" + # map (not masked), each entry exposed as an environment variable. Only what the tests require + # is passed - secrets: inherit is intentionally not used. Integration tests skip when + # CONFLUENCE_API_TOKEN is unavailable (forks / PRs without access). Secrets use the direct + # "${{ secrets.X }}" form (CodeQL-clean) and the whole blob is one folded line so GitHub + # registers a single mask. + TestData: >- + { "secrets": { "CONFLUENCE_API_TOKEN": "${{ secrets.CONFLUENCE_API_TOKEN }}" }, + "variables": { "CONFLUENCE_SITE": ${{ toJSON(vars.CONFLUENCE_SITE) }}, + "CONFLUENCE_USERNAME": ${{ toJSON(vars.CONFLUENCE_USERNAME) }}, + "CONFLUENCE_SPACE_KEY": ${{ toJSON(vars.CONFLUENCE_SPACE_KEY) }} } } diff --git a/README.md b/README.md index df91bec..e258743 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,51 @@ # Confluence -Confluence is intended to be a PowerShell module for interacting with Atlassian Confluence. +Confluence is a PowerShell module for interacting with the Atlassian Confluence Cloud REST API, both interactively and in automation. -## Status +It exposes pages, blog posts, folders, spaces, comments, labels, content properties, attachments, restrictions and more as PowerShell commands. +Every command is a thin wrapper over a single generic REST entry point (`Invoke-ConfluenceRestMethod`). Credentials and module configuration +are stored with the [Context](https://github.com/PSModule/Context) module, so you connect once and reuse the profile across sessions. -This repository is currently a placeholder. The module source still contains scaffold code and examples, so there are no supported Confluence-specific commands or usage examples to document yet. +## Installation + +Install the module from the PowerShell Gallery: + +```powershell +Install-PSResource -Name Confluence +Import-Module -Name Confluence +``` + +## Usage + +Connect with a scoped Atlassian API token, then call the resource commands. The token is kept as a `SecureString` in the Context vault. + +```powershell +# Connect and store a reusable, named credential profile +$token = Read-Host -AsSecureString # a scoped Atlassian API token +Connect-Confluence -Site 'yoursite' -Username 'you@example.com' -Token $token -SpaceKey 'DOCS' + +# Work with content +$space = Get-ConfluenceSpace -Key 'DOCS' +$page = New-ConfluencePage -SpaceId $space.id -Title 'Release notes' -Body '
Hello
' +Update-ConfluencePage -PageId $page.id -Body 'Updated
' +Get-ConfluencePageChild -PageId $page.id +``` + +See the [examples](examples) folder for more, including managing contexts and pages. + +The service-account token must be granted the module's required Confluence scopes — see [SCOPES.md](SCOPES.md). ## Documentation -When this module is implemented, command details should live in PowerShell help and generated documentation rather than being duplicated in this README. +Documentation is published at [psmodule.io/Confluence](https://psmodule.io/Confluence/). + +Use PowerShell help and command discovery for module details: + +```powershell +Get-Command -Module Confluence +Get-Help New-ConfluencePage -Examples +``` ## Contributing -Issues and pull requests are welcome when implementation work begins. +Issues and pull requests are welcome. Please use the repository issue tracker to report bugs, request features, or discuss improvements. diff --git a/SCOPES.md b/SCOPES.md new file mode 100644 index 0000000..b8e4da6 --- /dev/null +++ b/SCOPES.md @@ -0,0 +1,164 @@ +# Required Atlassian scopes + +`Confluence` authenticates with a **scoped** Atlassian API token (the `ATSTT…` +prefix) used as HTTP Basic authentication (`First draft
' + +# Read it back (storage format by default) +Get-ConfluencePage -PageId $page.id + +# Update the body (the version number is incremented automatically) +Update-ConfluencePage -PageId $page.id -Body 'Updated content
' + +# Add a child page, then list children and descendants +New-ConfluencePage -SpaceId $space.id -Title 'Details' -ParentId $page.id -Body 'Nested
' +Get-ConfluencePageChild -PageId $page.id +Get-ConfluenceDescendant -PageId $page.id + +# Labels and comments +Add-ConfluenceLabel -PageId $page.id -Label 'release', 'published' +Add-ConfluenceComment -PageId $page.id -Body 'Looks good.
' + +# Remove the whole subtree (moves the pages to trash) +Remove-ConfluencePage -PageId $page.id -Recurse diff --git a/src/finally.ps1 b/src/finally.ps1 deleted file mode 100644 index d8fc207..0000000 --- a/src/finally.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Write-Verbose '------------------------------' -Write-Verbose '--- THIS IS A LAST LOADER ---' -Write-Verbose '------------------------------' diff --git a/src/functions/private/Auth/Resolve-ConfluenceContext.ps1 b/src/functions/private/Auth/Resolve-ConfluenceContext.ps1 new file mode 100644 index 0000000..5f65563 --- /dev/null +++ b/src/functions/private/Auth/Resolve-ConfluenceContext.ps1 @@ -0,0 +1,39 @@ +function Resolve-ConfluenceContext { + <# + .SYNOPSIS + Resolve a context argument into a usable credential-context object. + + .DESCRIPTION + Accepts a context object/hashtable (returned as-is), a context name + (looked up in the vault), or nothing (falls back to the default context + recorded in the module configuration). + #> + [CmdletBinding()] + param( + # A context object, a context name, or $null for the default context. + [object]$Context + ) + + if ($null -ne $Context -and $Context -isnot [string]) { + return $Context + } + + Initialize-ConfluenceConfig + $id = if ([string]::IsNullOrEmpty([string]$Context)) { $script:Confluence.Config['DefaultContext'] } else { [string]$Context } + + if ([string]::IsNullOrEmpty($id)) { + throw 'No Confluence context was specified and no default context is set. Run Connect-Confluence first.' + } + + $resolved = $null + try { + $resolved = Get-Context -ID $id -Vault $script:Confluence.ContextVault -ErrorAction Stop + } catch { + $resolved = $null + } + + if (-not $resolved) { + throw "Confluence context '$id' was not found in vault '$($script:Confluence.ContextVault)'." + } + return $resolved +} diff --git a/src/functions/private/Auth/Resolve-ConfluenceToken.ps1 b/src/functions/private/Auth/Resolve-ConfluenceToken.ps1 new file mode 100644 index 0000000..5736dca --- /dev/null +++ b/src/functions/private/Auth/Resolve-ConfluenceToken.ps1 @@ -0,0 +1,22 @@ +function Resolve-ConfluenceToken { + <# + .SYNOPSIS + Return the plain-text API token from a SecureString or string. + + .DESCRIPTION + The token is stored as a SecureString in the credential context; this + converts it to the plain text needed to build the Basic auth header. + #> + [CmdletBinding()] + [OutputType([string])] + param( + # The token as a SecureString or a plain string. + [Parameter(Mandatory)] + [object]$Token + ) + + if ($Token -is [System.Security.SecureString]) { + return [System.Net.NetworkCredential]::new('', $Token).Password + } + return [string]$Token +} diff --git a/src/functions/private/Config/ConvertTo-ConfluenceHashtable.ps1 b/src/functions/private/Config/ConvertTo-ConfluenceHashtable.ps1 new file mode 100644 index 0000000..f90f38e --- /dev/null +++ b/src/functions/private/Config/ConvertTo-ConfluenceHashtable.ps1 @@ -0,0 +1,29 @@ +function ConvertTo-ConfluenceHashtable { + <# + .SYNOPSIS + Convert a PSCustomObject (or hashtable) into a plain hashtable. + + .DESCRIPTION + Used to turn the stored module-configuration context returned by + Get-Context into a mutable hashtable. + #> + [CmdletBinding()] + [OutputType([hashtable])] + param( + # The object to convert. + [Parameter(Mandatory)] + [object]$InputObject + ) + + if ($InputObject -is [hashtable]) { + # Return a shallow clone so callers can mutate the result without altering + # the source hashtable (e.g. the built-in $script:Confluence.DefaultConfig). + return $InputObject.Clone() + } + + $result = @{} + foreach ($property in $InputObject.PSObject.Properties) { + $result[$property.Name] = $property.Value + } + return $result +} diff --git a/src/functions/private/Config/Initialize-ConfluenceConfig.ps1 b/src/functions/private/Config/Initialize-ConfluenceConfig.ps1 new file mode 100644 index 0000000..d9b993c --- /dev/null +++ b/src/functions/private/Config/Initialize-ConfluenceConfig.ps1 @@ -0,0 +1,57 @@ +function Initialize-ConfluenceConfig { + <# + .SYNOPSIS + Load the module configuration from the context vault into memory. + + .DESCRIPTION + Loads the stored 'Module' configuration context, creating it from the + built-in defaults on first use. Missing default keys are backfilled. + #> + [CmdletBinding()] + param( + # Recreate the configuration from the built-in defaults. + [switch]$Force + ) + + if (-not $Force -and $null -ne $script:Confluence.Config) { + return + } + + $vault = $script:Confluence.ContextVault + $id = $script:Confluence.DefaultConfig.ID + + $stored = $null + if (-not $Force) { + try { + $stored = Get-Context -ID $id -Vault $vault -ErrorAction Stop + } catch { + $stored = $null + } + } + + $dirty = $false + if ($Force -or -not $stored) { + $config = ConvertTo-ConfluenceHashtable -InputObject $script:Confluence.DefaultConfig + $dirty = $true + } else { + $config = ConvertTo-ConfluenceHashtable -InputObject $stored + foreach ($key in $script:Confluence.DefaultConfig.Keys) { + if (-not $config.ContainsKey($key)) { + $config[$key] = $script:Confluence.DefaultConfig[$key] + $dirty = $true + } + } + } + + if ($config['ID'] -ne $id) { + $config['ID'] = $id + $dirty = $true + } + + # Only persist when we actually created or changed the stored config, to avoid + # unnecessary vault writes on every cmdlet call (this runs from most cmdlets). + if ($dirty) { + $null = Set-Context -ID $id -Context $config -Vault $vault + } + $script:Confluence.Config = $config +} diff --git a/src/functions/public/API/Invoke-ConfluenceRestMethod.ps1 b/src/functions/public/API/Invoke-ConfluenceRestMethod.ps1 new file mode 100644 index 0000000..5521733 --- /dev/null +++ b/src/functions/public/API/Invoke-ConfluenceRestMethod.ps1 @@ -0,0 +1,195 @@ +#Requires -Version 7.0 +#Requires -Modules @{ ModuleName = 'Context'; ModuleVersion = '8.1.6'; MaximumVersion = '8.999.999' } + +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Invoke-ConfluenceRestMethod { + <# + .SYNOPSIS + Call the Confluence REST API using a stored (or supplied) context. + + .DESCRIPTION + The single generic entry point that every other Confluence function is + built on. It resolves the credential context, builds the HTTP Basic auth + header, sends the request, and surfaces API errors as terminating errors. + With -All it transparently follows v2 cursor pagination and returns the + aggregated 'results'. Error messages include Atlassian's + 'X-Failure-Category' response header when present (for example + FAILURE_CLIENT_SCOPE_CHECK), which distinguishes a missing-scope + rejection from other failures. Pass -Debug to emit the full request and + response (status, headers, and body); the Authorization header is + redacted. + + .EXAMPLE + ```powershell + Invoke-ConfluenceRestMethod -ApiEndpoint '/wiki/api/v2/spaces' -Query @{ limit = 1 } + ``` + + Calls the spaces endpoint directly and returns the raw response. + + .LINK + https://psmodule.io/Confluence/Functions/API/Invoke-ConfluenceRestMethod/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/intro/ + + .LINK + https://developer.atlassian.com/cloud/confluence/scopes-for-oauth-2-3LO-and-forge-apps/ + #> + [CmdletBinding()] + [OutputType([object])] + param( + # The API path beginning with '/wiki/', e.g. '/wiki/api/v2/pages'. + # A full absolute URL is also accepted (used when following pagination links). + # When -ApiVersion is supplied this is treated as a path relative to that + # API family's root (e.g. -ApiVersion v2 -ApiEndpoint 'spaces'). + [Parameter(Mandatory)] + [string]$ApiEndpoint, + + # Optional API family. Uses the internal map to prepend the version prefix + # (v1 -> /wiki/rest/api, v2 -> /wiki/api/v2) to a relative -ApiEndpoint. + [ValidateSet('v1', 'v2')] + [string]$ApiVersion, + + # The HTTP method. Defaults to GET. + [ValidateSet('GET', 'POST', 'PUT', 'DELETE')] + [string]$Method = 'GET', + + # The request body as a hashtable/object (serialized to JSON) or a pre-serialized JSON string. + [object]$Body, + + # A multipart/form-data payload (used for attachment uploads). + [hashtable]$Form, + + # Query-string parameters as a hashtable. + [hashtable]$Query, + + # Follow pagination and return every item from the 'results' collections. + [switch]$All, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + if ($All -and $Method -ne 'GET') { + throw "The -All switch follows pagination and is only valid for GET requests, not $Method." + } + + # Resolve a relative endpoint against the internal v1/v2 map when a version is given. + if ($ApiVersion -and $ApiEndpoint -notmatch '^https?://') { + $ApiEndpoint = '{0}/{1}' -f $script:Confluence.ApiPaths[$ApiVersion], $ApiEndpoint.TrimStart('/') + } + + $resolved = Resolve-ConfluenceContext -Context $Context + $token = Resolve-ConfluenceToken -Token $resolved.Token + $pair = '{0}:{1}' -f $resolved.Username, $token + $authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair)) + + # For v2 collection endpoints, default the page size from the PerPage config so + # -All pages in configured-sized batches instead of Confluence's small server + # default (~25), unless the caller passed an explicit limit. Absolute _links.next + # URLs and v1 endpoints are left untouched. + $effectiveQuery = if ($Query) { @{} + $Query } else { @{} } + if ($All -and $ApiEndpoint -like '/wiki/api/v2/*' -and -not ($effectiveQuery.Keys | Where-Object { $_ -ieq 'limit' })) { + Initialize-ConfluenceConfig + $perPage = $script:Confluence.Config['PerPage'] + if ($perPage) { + $effectiveQuery['limit'] = $perPage + } + } + + $endpoint = $ApiEndpoint + if ($effectiveQuery.Count -gt 0) { + $pairs = foreach ($entry in $effectiveQuery.GetEnumerator()) { + '{0}={1}' -f [uri]::EscapeDataString([string]$entry.Key), [uri]::EscapeDataString([string]$entry.Value) + } + $separator = if ($endpoint.Contains('?')) { '&' } else { '?' } + $endpoint = '{0}{1}{2}' -f $endpoint, $separator, ($pairs -join '&') + } + + $paginate = $All.IsPresent + $debug = $DebugPreference -eq 'Continue' + + do { + $uri = if ($endpoint -match '^https?://') { $endpoint } else { '{0}{1}' -f $resolved.ApiBaseUri, $endpoint } + + $headers = @{ + Authorization = $authorization + Accept = 'application/json' + } + + $request = @{ + Uri = $uri + Method = $Method + Headers = $headers + SkipHttpErrorCheck = $true + StatusCodeVariable = 'statusCode' + ResponseHeadersVariable = 'responseHeaders' + } + + if ($Form) { + $headers['X-Atlassian-Token'] = 'no-check' + $request['Form'] = $Form + } elseif ($null -ne $Body) { + $request['ContentType'] = 'application/json' + # Serialize with -InputObject rather than the pipeline: piping a + # single-element array to ConvertTo-Json emits a bare object and loses + # the array, whereas -InputObject serializes arrays and objects faithfully. + $request['Body'] = if ($Body -is [string]) { $Body } else { ConvertTo-Json -InputObject $Body -Depth 20 -Compress } + } + + if ($debug) { + Write-Debug "Request: $Method $uri" + foreach ($headerName in ($headers.Keys | Sort-Object)) { + $headerValue = if ($headerName -eq 'Authorization') { 'BasicNice page.
' + ``` + + Adds a footer comment to page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/Comments/Add-ConfluenceComment/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-comment/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The page id to comment on. + [Parameter(Mandatory)] + [string]$PageId, + + # The comment body. + [Parameter(Mandatory)] + [string]$Body, + + # The body representation. Defaults to 'storage'. + [ValidateSet('storage', 'atlas_doc_format', 'wiki')] + [string]$Representation = 'storage', + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + $payload = @{ + pageId = $PageId + body = @{ + representation = $Representation + value = $Body + } + } + + if ($PSCmdlet.ShouldProcess($PageId, 'Add footer comment')) { + Invoke-ConfluenceRestMethod -ApiEndpoint '/wiki/api/v2/footer-comments' -Method 'POST' -Body $payload -Context $Context + } +} diff --git a/src/functions/public/Comments/Get-ConfluenceComment.ps1 b/src/functions/public/Comments/Get-ConfluenceComment.ps1 new file mode 100644 index 0000000..78684f4 --- /dev/null +++ b/src/functions/public/Comments/Get-ConfluenceComment.ps1 @@ -0,0 +1,34 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceComment { + <# + .SYNOPSIS + List the footer comments on a page (read:comment). + + .DESCRIPTION + Returns every footer comment on the page, following pagination. + + .EXAMPLE + ```powershell + Get-ConfluenceComment -PageId '12345' + ``` + + Lists the footer comments on page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/Comments/Get-ConfluenceComment/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-comment/ + #> + [CmdletBinding()] + param( + # The page id. + [Parameter(Mandatory)] + [string]$PageId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/pages/$PageId/footer-comments" -All -Context $Context +} diff --git a/src/functions/public/Comments/Remove-ConfluenceComment.ps1 b/src/functions/public/Comments/Remove-ConfluenceComment.ps1 new file mode 100644 index 0000000..9a92a74 --- /dev/null +++ b/src/functions/public/Comments/Remove-ConfluenceComment.ps1 @@ -0,0 +1,36 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Remove-ConfluenceComment { + <# + .SYNOPSIS + Delete a footer comment (delete:comment). + + .DESCRIPTION + Permanently deletes a footer comment by id. + + .EXAMPLE + ```powershell + Remove-ConfluenceComment -CommentId '55555' + ``` + + Deletes the footer comment with ID 55555. + + .LINK + https://psmodule.io/Confluence/Functions/Comments/Remove-ConfluenceComment/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-comment/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The footer comment id. + [Parameter(Mandatory)] + [string]$CommentId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + if ($PSCmdlet.ShouldProcess($CommentId, 'Delete footer comment')) { + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/footer-comments/$CommentId" -Method 'DELETE' -Context $Context + } +} diff --git a/src/functions/public/Config/Get-ConfluenceConfig.ps1 b/src/functions/public/Config/Get-ConfluenceConfig.ps1 new file mode 100644 index 0000000..8057009 --- /dev/null +++ b/src/functions/public/Config/Get-ConfluenceConfig.ps1 @@ -0,0 +1,32 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceConfig { + <# + .SYNOPSIS + Get the Confluence module configuration. + + .DESCRIPTION + Returns the whole configuration hashtable, or the value of a single + named configuration item. + + .EXAMPLE + ```powershell + Get-ConfluenceConfig -Name 'DefaultContext' + ``` + + Gets the DefaultContext value from the module configuration. + + .LINK + https://psmodule.io/Confluence/Functions/Config/Get-ConfluenceConfig/ + #> + [CmdletBinding()] + param( + # The configuration item to return. If omitted, the whole config is returned. + [string]$Name + ) + + Initialize-ConfluenceConfig + if ([string]::IsNullOrEmpty($Name)) { + return $script:Confluence.Config + } + $script:Confluence.Config[$Name] +} diff --git a/src/functions/public/Config/Set-ConfluenceConfig.ps1 b/src/functions/public/Config/Set-ConfluenceConfig.ps1 new file mode 100644 index 0000000..51d0343 --- /dev/null +++ b/src/functions/public/Config/Set-ConfluenceConfig.ps1 @@ -0,0 +1,38 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Set-ConfluenceConfig { + <# + .SYNOPSIS + Set a Confluence module configuration value. + + .DESCRIPTION + Updates a single configuration item and persists the configuration to + the context vault. + + .EXAMPLE + ```powershell + Set-ConfluenceConfig -Name 'PerPage' -Value 50 + ``` + + Sets the PerPage configuration value to 50. + + .LINK + https://psmodule.io/Confluence/Functions/Config/Set-ConfluenceConfig/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The configuration item name. + [Parameter(Mandatory)] + [string]$Name, + + # The value to store. + [Parameter(Mandatory)] + [AllowEmptyString()] + [object]$Value + ) + + Initialize-ConfluenceConfig + if ($PSCmdlet.ShouldProcess("Confluence config '$Name'", 'Set')) { + $script:Confluence.Config[$Name] = $Value + $null = Set-Context -ID $script:Confluence.DefaultConfig.ID -Context $script:Confluence.Config -Vault $script:Confluence.ContextVault + } +} diff --git a/src/functions/public/ContentProperties/Get-ConfluenceContentProperty.ps1 b/src/functions/public/ContentProperties/Get-ConfluenceContentProperty.ps1 new file mode 100644 index 0000000..ee315eb --- /dev/null +++ b/src/functions/public/ContentProperties/Get-ConfluenceContentProperty.ps1 @@ -0,0 +1,44 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceContentProperty { + <# + .SYNOPSIS + Get content properties of a page (read:page). + + .DESCRIPTION + Returns all content properties on a page, or the single property with the + given key. In v2, a page's content properties are governed by the page's + own scope (read:page); the read:content.property scope applies to the v1 + property API. + + .EXAMPLE + ```powershell + Get-ConfluenceContentProperty -PageId '12345' -Key 'my-prop' + ``` + + Gets the 'my-prop' content property from page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/ContentProperties/Get-ConfluenceContentProperty/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-content-properties/ + #> + [CmdletBinding()] + param( + # The page id. + [Parameter(Mandatory)] + [string]$PageId, + + # The property key to return. If omitted, all properties are returned. + [string]$Key, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + $all = Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/pages/$PageId/properties" -All -Context $Context + if ([string]::IsNullOrEmpty($Key)) { + return $all + } + $all | Where-Object { $_.key -eq $Key } | Select-Object -First 1 +} diff --git a/src/functions/public/ContentProperties/Remove-ConfluenceContentProperty.ps1 b/src/functions/public/ContentProperties/Remove-ConfluenceContentProperty.ps1 new file mode 100644 index 0000000..6621e58 --- /dev/null +++ b/src/functions/public/ContentProperties/Remove-ConfluenceContentProperty.ps1 @@ -0,0 +1,41 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Remove-ConfluenceContentProperty { + <# + .SYNOPSIS + Delete a content property from a page (read:page and write:page). + + .DESCRIPTION + Deletes a content property by its id. In v2, page content properties are + governed by the page's own scope (read:page and write:page). + + .EXAMPLE + ```powershell + Remove-ConfluenceContentProperty -PageId '12345' -PropertyId '98765' + ``` + + Deletes the content property with ID 98765 from page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/ContentProperties/Remove-ConfluenceContentProperty/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-content-properties/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The page id. + [Parameter(Mandatory)] + [string]$PageId, + + # The content-property id. + [Parameter(Mandatory)] + [string]$PropertyId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + if ($PSCmdlet.ShouldProcess("$PageId/$PropertyId", 'Delete content property')) { + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/pages/$PageId/properties/$PropertyId" -Method 'DELETE' -Context $Context + } +} diff --git a/src/functions/public/ContentProperties/Set-ConfluenceContentProperty.ps1 b/src/functions/public/ContentProperties/Set-ConfluenceContentProperty.ps1 new file mode 100644 index 0000000..9191726 --- /dev/null +++ b/src/functions/public/ContentProperties/Set-ConfluenceContentProperty.ps1 @@ -0,0 +1,67 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Set-ConfluenceContentProperty { + <# + .SYNOPSIS + Create or update a content property on a page (read:page and write:page). + + .DESCRIPTION + Creates the property if it does not exist, or updates it (incrementing + its version) if it does. In v2, page content properties are governed by + the page's own scope: reading requires read:page and writing requires + write:page. + + .EXAMPLE + ```powershell + Set-ConfluenceContentProperty -PageId '12345' -Key 'owner' -Value @{ team = 'ai' } + ``` + + Creates or updates the 'owner' content property on page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/ContentProperties/Set-ConfluenceContentProperty/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-content-properties/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The page id. + [Parameter(Mandatory)] + [string]$PageId, + + # The property key. + [Parameter(Mandatory)] + [string]$Key, + + # The property value (any JSON-serializable object). + [Parameter(Mandatory)] + [object]$Value, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + $existing = Get-ConfluenceContentProperty -PageId $PageId -Key $Key -Context $Context + + if ($existing) { + $payload = @{ + key = $Key + value = $Value + version = @{ + number = [int]$existing.version.number + 1 + } + } + $endpoint = "/wiki/api/v2/pages/$PageId/properties/$($existing.id)" + if ($PSCmdlet.ShouldProcess("$PageId/$Key", 'Update content property')) { + Invoke-ConfluenceRestMethod -ApiEndpoint $endpoint -Method 'PUT' -Body $payload -Context $Context + } + } else { + $payload = @{ + key = $Key + value = $Value + } + if ($PSCmdlet.ShouldProcess("$PageId/$Key", 'Create content property')) { + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/pages/$PageId/properties" -Method 'POST' -Body $payload -Context $Context + } + } +} diff --git a/src/functions/public/Folders/Get-ConfluenceFolder.ps1 b/src/functions/public/Folders/Get-ConfluenceFolder.ps1 new file mode 100644 index 0000000..f9a03ec --- /dev/null +++ b/src/functions/public/Folders/Get-ConfluenceFolder.ps1 @@ -0,0 +1,34 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceFolder { + <# + .SYNOPSIS + Get a Confluence folder by id (read:folder). + + .DESCRIPTION + Returns a single folder. + + .EXAMPLE + ```powershell + Get-ConfluenceFolder -FolderId '67890' + ``` + + Gets the folder with ID 67890. + + .LINK + https://psmodule.io/Confluence/Functions/Folders/Get-ConfluenceFolder/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-folder/ + #> + [CmdletBinding()] + param( + # The folder id. + [Parameter(Mandatory)] + [string]$FolderId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/folders/$FolderId" -Context $Context +} diff --git a/src/functions/public/Folders/New-ConfluenceFolder.ps1 b/src/functions/public/Folders/New-ConfluenceFolder.ps1 new file mode 100644 index 0000000..73ecedb --- /dev/null +++ b/src/functions/public/Folders/New-ConfluenceFolder.ps1 @@ -0,0 +1,51 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function New-ConfluenceFolder { + <# + .SYNOPSIS + Create a Confluence folder (write:folder). + + .DESCRIPTION + Creates a folder in a space, optionally beneath a parent page or folder. + + .EXAMPLE + ```powershell + New-ConfluenceFolder -SpaceId $spaceId -Title 'Archive' -ParentId $pageId + ``` + + Creates a folder named 'Archive' beneath the given parent. + + .LINK + https://psmodule.io/Confluence/Functions/Folders/New-ConfluenceFolder/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-folder/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The id of the space to create the folder in. + [Parameter(Mandatory)] + [string]$SpaceId, + + # The folder title. + [Parameter(Mandatory)] + [string]$Title, + + # The id of the parent page or folder. Omit to create at the space root. + [string]$ParentId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + $payload = @{ + spaceId = $SpaceId + title = $Title + } + if (-not [string]::IsNullOrEmpty($ParentId)) { + $payload['parentId'] = $ParentId + } + + if ($PSCmdlet.ShouldProcess($Title, 'Create Confluence folder')) { + Invoke-ConfluenceRestMethod -ApiEndpoint '/wiki/api/v2/folders' -Method 'POST' -Body $payload -Context $Context + } +} diff --git a/src/functions/public/Folders/Remove-ConfluenceFolder.ps1 b/src/functions/public/Folders/Remove-ConfluenceFolder.ps1 new file mode 100644 index 0000000..19dc6a8 --- /dev/null +++ b/src/functions/public/Folders/Remove-ConfluenceFolder.ps1 @@ -0,0 +1,36 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Remove-ConfluenceFolder { + <# + .SYNOPSIS + Delete a Confluence folder (delete:folder). + + .DESCRIPTION + Moves a folder to the trash. + + .EXAMPLE + ```powershell + Remove-ConfluenceFolder -FolderId '67890' + ``` + + Moves the folder with ID 67890 to the trash. + + .LINK + https://psmodule.io/Confluence/Functions/Folders/Remove-ConfluenceFolder/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-folder/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The folder id to delete. + [Parameter(Mandatory)] + [string]$FolderId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + if ($PSCmdlet.ShouldProcess($FolderId, 'Delete Confluence folder')) { + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/folders/$FolderId" -Method 'DELETE' -Context $Context + } +} diff --git a/src/functions/public/Get-PSModuleTest.ps1 b/src/functions/public/Get-PSModuleTest.ps1 deleted file mode 100644 index ffe3483..0000000 --- a/src/functions/public/Get-PSModuleTest.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -#Requires -Modules Utilities - -function Get-PSModuleTest { - <# - .SYNOPSIS - Performs tests on a module. - - .DESCRIPTION - Performs tests on a module. - - .EXAMPLE - Test-PSModule -Name 'World' - - "Hello, World!" - #> - [CmdletBinding()] - param ( - # Name of the person to greet. - [Parameter(Mandatory)] - [string] $Name - ) - Write-Output "Hello, $Name!" -} diff --git a/src/functions/public/Labels/Add-ConfluenceLabel.ps1 b/src/functions/public/Labels/Add-ConfluenceLabel.ps1 new file mode 100644 index 0000000..1558f79 --- /dev/null +++ b/src/functions/public/Labels/Add-ConfluenceLabel.ps1 @@ -0,0 +1,55 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Add-ConfluenceLabel { + <# + .SYNOPSIS + Add one or more labels to a page (read:label and write:label). + + .DESCRIPTION + Adds labels to a page. Confluence exposes label creation only on the v1 + content endpoint (/wiki/rest/api/content/{id}/label), which the granular + read:label and write:label scopes authorise. + + .EXAMPLE + ```powershell + Add-ConfluenceLabel -PageId '12345' -Label 'docs', 'published' + ``` + + Adds the 'docs' and 'published' labels to page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/Labels/Add-ConfluenceLabel/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content-labels/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The page (content) id to label. + [Parameter(Mandatory)] + [string]$PageId, + + # One or more label names to add. + [Parameter(Mandatory)] + [string[]]$Label, + + # The label prefix. Defaults to 'global'. + [string]$Prefix = 'global', + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + $payload = foreach ($name in $Label) { + @{ + prefix = $Prefix + name = $name + } + } + # @(...) forces an array (a single label is otherwise a scalar) and -InputObject + # avoids the pipeline, so the v1 label endpoint always receives a JSON array. + $json = ConvertTo-Json -InputObject @($payload) -Depth 5 -Compress + + if ($PSCmdlet.ShouldProcess($PageId, "Add label(s): $($Label -join ', ')")) { + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/rest/api/content/$PageId/label" -Method 'POST' -Body $json -Context $Context + } +} diff --git a/src/functions/public/Labels/Get-ConfluenceLabel.ps1 b/src/functions/public/Labels/Get-ConfluenceLabel.ps1 new file mode 100644 index 0000000..325f779 --- /dev/null +++ b/src/functions/public/Labels/Get-ConfluenceLabel.ps1 @@ -0,0 +1,36 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceLabel { + <# + .SYNOPSIS + List the labels on a page (read:page). + + .DESCRIPTION + Returns every label on the page, following pagination. In v2 a page's + labels are read under the page's own scope (read:page); the read:label + scope covers only the site-wide GET /labels endpoint. + + .EXAMPLE + ```powershell + Get-ConfluenceLabel -PageId '12345' + ``` + + Lists the labels on page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/Labels/Get-ConfluenceLabel/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-label/ + #> + [CmdletBinding()] + param( + # The page id. + [Parameter(Mandatory)] + [string]$PageId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/pages/$PageId/labels" -All -Context $Context +} diff --git a/src/functions/public/Labels/Remove-ConfluenceLabel.ps1 b/src/functions/public/Labels/Remove-ConfluenceLabel.ps1 new file mode 100644 index 0000000..d6dabf2 --- /dev/null +++ b/src/functions/public/Labels/Remove-ConfluenceLabel.ps1 @@ -0,0 +1,40 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Remove-ConfluenceLabel { + <# + .SYNOPSIS + Remove a label from a page (write:label). + + .DESCRIPTION + Removes a label from a page via the v1 content endpoint. + + .EXAMPLE + ```powershell + Remove-ConfluenceLabel -PageId '12345' -Label 'docs' + ``` + + Removes the 'docs' label from page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/Labels/Remove-ConfluenceLabel/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content-labels/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The page (content) id. + [Parameter(Mandatory)] + [string]$PageId, + + # The label name to remove. + [Parameter(Mandatory)] + [string]$Label, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + if ($PSCmdlet.ShouldProcess($PageId, "Remove label: $Label")) { + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/rest/api/content/$PageId/label" -Method 'DELETE' -Query @{ name = $Label } -Context $Context + } +} diff --git a/src/functions/public/Pages/Get-ConfluenceDescendant.ps1 b/src/functions/public/Pages/Get-ConfluenceDescendant.ps1 new file mode 100644 index 0000000..94794ab --- /dev/null +++ b/src/functions/public/Pages/Get-ConfluenceDescendant.ps1 @@ -0,0 +1,35 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceDescendant { + <# + .SYNOPSIS + List all descendants of a page (read:hierarchical-content). + + .DESCRIPTION + Returns every descendant (child, grandchild, ...) of a page, following + pagination automatically. + + .EXAMPLE + ```powershell + Get-ConfluenceDescendant -PageId '12345' + ``` + + Lists all descendants of page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/Pages/Get-ConfluenceDescendant/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-descendants/ + #> + [CmdletBinding()] + param( + # The ancestor page id. + [Parameter(Mandatory)] + [string]$PageId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/pages/$PageId/descendants" -All -Context $Context +} diff --git a/src/functions/public/Pages/Get-ConfluencePage.ps1 b/src/functions/public/Pages/Get-ConfluencePage.ps1 new file mode 100644 index 0000000..5fd019a --- /dev/null +++ b/src/functions/public/Pages/Get-ConfluencePage.ps1 @@ -0,0 +1,38 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluencePage { + <# + .SYNOPSIS + Get a Confluence page by id (read:page). + + .DESCRIPTION + Returns a single page, including its body in the requested format. + + .EXAMPLE + ```powershell + Get-ConfluencePage -PageId '12345' + ``` + + Gets page 12345, including its body in storage format. + + .LINK + https://psmodule.io/Confluence/Functions/Pages/Get-ConfluencePage/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/ + #> + [CmdletBinding()] + param( + # The page id. + [Parameter(Mandatory)] + [string]$PageId, + + # The body format to return. Defaults to 'storage'. + [ValidateSet('storage', 'atlas_doc_format', 'view', 'export_view', 'anonymous_export_view', 'styled_view', 'editor')] + [string]$BodyFormat = 'storage', + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/pages/$PageId" -Query @{ 'body-format' = $BodyFormat } -Context $Context +} diff --git a/src/functions/public/Pages/Get-ConfluencePageChild.ps1 b/src/functions/public/Pages/Get-ConfluencePageChild.ps1 new file mode 100644 index 0000000..859556e --- /dev/null +++ b/src/functions/public/Pages/Get-ConfluencePageChild.ps1 @@ -0,0 +1,34 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluencePageChild { + <# + .SYNOPSIS + List the direct child pages of a page (read:page). + + .DESCRIPTION + Returns every direct child page, following pagination automatically. + + .EXAMPLE + ```powershell + Get-ConfluencePageChild -PageId '12345' + ``` + + Lists the direct child pages of page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/Pages/Get-ConfluencePageChild/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-children/ + #> + [CmdletBinding()] + param( + # The parent page id. + [Parameter(Mandatory)] + [string]$PageId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/pages/$PageId/children" -All -Context $Context +} diff --git a/src/functions/public/Pages/Get-ConfluencePageVersion.ps1 b/src/functions/public/Pages/Get-ConfluencePageVersion.ps1 new file mode 100644 index 0000000..62b40c3 --- /dev/null +++ b/src/functions/public/Pages/Get-ConfluencePageVersion.ps1 @@ -0,0 +1,34 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluencePageVersion { + <# + .SYNOPSIS + List the version history of a page (read:page). + + .DESCRIPTION + Returns every version entry for a page, newest first, following pagination. + + .EXAMPLE + ```powershell + Get-ConfluencePageVersion -PageId '12345' + ``` + + Lists the version history of page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/Pages/Get-ConfluencePageVersion/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-version/ + #> + [CmdletBinding()] + param( + # The page id. + [Parameter(Mandatory)] + [string]$PageId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/pages/$PageId/versions" -All -Context $Context +} diff --git a/src/functions/public/Pages/New-ConfluencePage.ps1 b/src/functions/public/Pages/New-ConfluencePage.ps1 new file mode 100644 index 0000000..421a7ec --- /dev/null +++ b/src/functions/public/Pages/New-ConfluencePage.ps1 @@ -0,0 +1,64 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function New-ConfluencePage { + <# + .SYNOPSIS + Create a Confluence page (write:page). + + .DESCRIPTION + Creates a page in a space, optionally beneath a parent page. A page with + no parent is created under the space home page. + + .EXAMPLE + ```powershell + New-ConfluencePage -SpaceId $spaceId -Title 'Docs' -Body 'Hello
' + ``` + + Creates a page titled 'Docs' in the given space. + + .LINK + https://psmodule.io/Confluence/Functions/Pages/New-ConfluencePage/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The id of the space to create the page in. + [Parameter(Mandatory)] + [string]$SpaceId, + + # The page title (must be unique within the space). + [Parameter(Mandatory)] + [string]$Title, + + # The id of the parent page. Omit to create a top-level page. + [string]$ParentId, + + # The page body in the chosen representation. Defaults to empty. + [string]$Body = '', + + # The body representation. Defaults to 'storage'. + [ValidateSet('storage', 'atlas_doc_format', 'wiki')] + [string]$Representation = 'storage', + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + $payload = @{ + spaceId = $SpaceId + status = 'current' + title = $Title + body = @{ + representation = $Representation + value = $Body + } + } + if (-not [string]::IsNullOrEmpty($ParentId)) { + $payload['parentId'] = $ParentId + } + + if ($PSCmdlet.ShouldProcess($Title, 'Create Confluence page')) { + Invoke-ConfluenceRestMethod -ApiEndpoint '/wiki/api/v2/pages' -Method 'POST' -Body $payload -Context $Context + } +} diff --git a/src/functions/public/Pages/Remove-ConfluencePage.ps1 b/src/functions/public/Pages/Remove-ConfluencePage.ps1 new file mode 100644 index 0000000..a57d02a --- /dev/null +++ b/src/functions/public/Pages/Remove-ConfluencePage.ps1 @@ -0,0 +1,68 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Remove-ConfluencePage { + <# + .SYNOPSIS + Delete a Confluence page (delete:page). + + .DESCRIPTION + Moves a page to the trash. With -Recurse, direct and nested child pages + are removed first so that a whole subtree can be deleted. With -Purge the + page is permanently deleted: because Confluence requires a page to be in + the trash before it can be purged, the page is trashed first and then + purged in a second call. + + .EXAMPLE + ```powershell + Remove-ConfluencePage -PageId '12345' -Recurse + ``` + + Moves page 12345 and its child pages to the trash. + + .LINK + https://psmodule.io/Confluence/Functions/Pages/Remove-ConfluencePage/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The page id to delete. + [Parameter(Mandatory)] + [string]$PageId, + + # Remove child pages before removing the page itself. + [switch]$Recurse, + + # Permanently delete the page. It is moved to the trash first (as the API + # requires) and then purged. + [switch]$Purge, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + if ($Recurse) { + $children = Get-ConfluencePageChild -PageId $PageId -Context $Context + foreach ($child in $children) { + Remove-ConfluencePage -PageId $child.id -Recurse -Purge:$Purge -Context $Context + } + } + + $base = "/wiki/api/v2/pages/$PageId" + $action = if ($Purge) { 'Permanently delete Confluence page' } else { 'Delete Confluence page' } + + if ($PSCmdlet.ShouldProcess($PageId, $action)) { + if ($Purge) { + # A page must be in the trash before it can be purged. Trash first + # (ignoring failures, e.g. when it is already trashed), then purge. + try { + Invoke-ConfluenceRestMethod -ApiEndpoint $base -Method 'DELETE' -Context $Context + } catch { + Write-Verbose "Trash step before purge failed (the page may already be trashed): $($_.Exception.Message)" + } + Invoke-ConfluenceRestMethod -ApiEndpoint ('{0}?purge=true' -f $base) -Method 'DELETE' -Context $Context + } else { + Invoke-ConfluenceRestMethod -ApiEndpoint $base -Method 'DELETE' -Context $Context + } + } +} diff --git a/src/functions/public/Pages/Update-ConfluencePage.ps1 b/src/functions/public/Pages/Update-ConfluencePage.ps1 new file mode 100644 index 0000000..da36da3 --- /dev/null +++ b/src/functions/public/Pages/Update-ConfluencePage.ps1 @@ -0,0 +1,71 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Update-ConfluencePage { + <# + .SYNOPSIS + Update a Confluence page (read:page and write:page). + + .DESCRIPTION + Updates the title and/or body of a page, automatically incrementing the + version number. Unspecified fields keep their current values. The current + page is read first (read:page) to preserve unspecified fields and to + obtain the version number, then written back (write:page). + + .EXAMPLE + ```powershell + Update-ConfluencePage -PageId '12345' -Body 'Updated
' + ``` + + Updates the body of page 12345, incrementing its version. + + .LINK + https://psmodule.io/Confluence/Functions/Pages/Update-ConfluencePage/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The page id to update. + [Parameter(Mandatory)] + [string]$PageId, + + # The new title. Defaults to the existing title. + [string]$Title, + + # The new body. Defaults to the existing body. + [string]$Body, + + # The body representation. Defaults to 'storage'. + [ValidateSet('storage', 'atlas_doc_format', 'wiki')] + [string]$Representation = 'storage', + + # The page status. Defaults to the page's current status. + [string]$Status, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + $current = Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/pages/$PageId" -Query @{ 'body-format' = $Representation } -Context $Context + + $newTitle = if ($PSBoundParameters.ContainsKey('Title')) { $Title } else { $current.title } + $newBody = if ($PSBoundParameters.ContainsKey('Body')) { $Body } else { $current.body.$Representation.value } + $newStatus = if ($PSBoundParameters.ContainsKey('Status')) { $Status } else { $current.status } + + $payload = @{ + id = $PageId + status = $newStatus + title = $newTitle + version = @{ + number = [int]$current.version.number + 1 + } + body = @{ + representation = $Representation + value = $newBody + } + } + + if ($PSCmdlet.ShouldProcess($PageId, 'Update Confluence page')) { + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/pages/$PageId" -Method 'PUT' -Body $payload -Context $Context + } +} diff --git a/src/functions/public/Restrictions/Get-ConfluenceRestriction.ps1 b/src/functions/public/Restrictions/Get-ConfluenceRestriction.ps1 new file mode 100644 index 0000000..268d903 --- /dev/null +++ b/src/functions/public/Restrictions/Get-ConfluenceRestriction.ps1 @@ -0,0 +1,40 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceRestriction { + <# + .SYNOPSIS + Get the content restrictions on a page (read:content-details). + + .DESCRIPTION + Returns the read/update restrictions applied to a page. Reading + restrictions requires read:content-details; the read:content.restriction + scope only covers the narrow byOperation/{op}/user|group status checks. + Content restrictions are part of the v1 content API (see the link). + + .EXAMPLE + ```powershell + Get-ConfluenceRestriction -PageId '12345' + ``` + + Gets the read/update restrictions on page 12345. + + .LINK + https://psmodule.io/Confluence/Functions/Restrictions/Get-ConfluenceRestriction/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content-restrictions/ + #> + [CmdletBinding()] + param( + # The page id. + [Parameter(Mandatory)] + [string]$PageId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + # Content restrictions live on the v1 content API; there is no equivalent v2 + # endpoint (a v2 '/pages/{id}/restrictions' path is rejected with a scope + # check). The v1 endpoint returns the read/update operations in 'results'. + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/rest/api/content/$PageId/restriction" -All -Context $Context +} diff --git a/src/functions/public/Site/Get-ConfluenceCloudId.ps1 b/src/functions/public/Site/Get-ConfluenceCloudId.ps1 new file mode 100644 index 0000000..dba457f --- /dev/null +++ b/src/functions/public/Site/Get-ConfluenceCloudId.ps1 @@ -0,0 +1,92 @@ +#SkipTest:FunctionTest:Resolves the cloud ID from the public tenant_info endpoint; covered by integration tests. +function Get-ConfluenceCloudId { + <# + .SYNOPSIS + Get the cloud ID for a Confluence Cloud site. + + .DESCRIPTION + Looks up the cloud ID for an Atlassian Confluence Cloud site from the + public `/_edge/tenant_info` endpoint (a GET request), so a caller can supply + a site name or URL instead of the cloud ID. Accepts a bare subdomain + (`myorg`), a host (`myorg.atlassian.net`), or any URL on the site + (`https://myorg.atlassian.net/wiki/...`). No authentication is required. + + .EXAMPLE + ```powershell + Get-ConfluenceCloudId -Site 'msxorg' + ``` + + Returns the cloud ID for `https://msxorg.atlassian.net`. + + .EXAMPLE + ```powershell + 'https://msxorg.atlassian.net/wiki/spaces/DOCS' | Get-ConfluenceCloudId + ``` + + Resolves the cloud ID from a full site URL supplied through the pipeline. + + .EXAMPLE + ```powershell + Connect-Confluence -CloudId (Get-ConfluenceCloudId -Site 'msxorg') -Username $user -Token $token + ``` + + Uses the resolved cloud ID to connect, so the caller only needs the site name. + + .OUTPUTS + System.String + + .LINK + https://psmodule.io/Confluence/Functions/Site/Get-ConfluenceCloudId/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/intro/#about + #> + [CmdletBinding()] + [OutputType([string])] + param( + # The site to resolve: a subdomain (`myorg`), a host (`myorg.atlassian.net`), + # or any URL on the site (`https://myorg.atlassian.net/...`). + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [Alias('Url', 'Uri', 'Name')] + [string]$Site + ) + + process { + $trimmed = $Site.Trim() + if ([string]::IsNullOrWhiteSpace($trimmed)) { + throw 'The -Site value cannot be empty.' + } + + # Derive the host whether the caller passed a bare subdomain, a host, or a URL. + try { + $siteHost = if ($trimmed -match '^[A-Za-z][A-Za-z0-9+.-]*://') { + ([uri]$trimmed).Host + } elseif ($trimmed -match '[/.]') { + ([uri]"https://$trimmed").Host + } else { + "$trimmed.atlassian.net" + } + } catch { + throw "Could not determine the site host from '$Site': $($_.Exception.Message)" + } + + if ([string]::IsNullOrWhiteSpace($siteHost)) { + throw "Could not determine the site host from '$Site'." + } + + $tenantInfoUri = "https://$siteHost/_edge/tenant_info" + Write-Verbose "Resolving cloud ID from [$tenantInfoUri]." + + try { + $tenantInfo = Invoke-RestMethod -Uri $tenantInfoUri -Method Get -ErrorAction Stop + } catch { + throw "Failed to resolve the cloud ID for '$siteHost' from [$tenantInfoUri]: $($_.Exception.Message)" + } + + if ([string]::IsNullOrWhiteSpace($tenantInfo.cloudId)) { + throw "The endpoint [$tenantInfoUri] did not return a cloud ID for '$siteHost'." + } + + $tenantInfo.cloudId + } +} diff --git a/src/functions/public/Site/Get-ConfluenceSiteInfo.ps1 b/src/functions/public/Site/Get-ConfluenceSiteInfo.ps1 new file mode 100644 index 0000000..9fe3138 --- /dev/null +++ b/src/functions/public/Site/Get-ConfluenceSiteInfo.ps1 @@ -0,0 +1,61 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceSiteInfo { + <# + .SYNOPSIS + Get basic information about the connected Confluence site (read:space or read:page). + + .DESCRIPTION + Returns the site's cloud id (parsed from the API-gateway base URI) and + the browsable site (wiki) base URL (read from the API's _links.base). A + scoped v2 token cannot reach the dedicated site/settings endpoints (site + name, edition, build number) — those are v1-only and are rejected. + + .EXAMPLE + ```powershell + Get-ConfluenceSiteInfo + ``` + + Returns the cloud ID and browsable site URL for the connected site. + + .LINK + https://psmodule.io/Confluence/Functions/Site/Get-ConfluenceSiteInfo/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-space/ + #> + [CmdletBinding()] + [OutputType([pscustomobject])] + param( + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + $resolved = Resolve-ConfluenceContext -Context $Context + $cloudId = if ($resolved.ApiBaseUri -match '/ex/confluence/([0-9a-fA-F-]+)') { $Matches[1] } else { $null } + + # _links.base (the browsable site URL) appears on any v2 collection response; + # try a couple so this works whether the token has read:space or only read:page. + $siteUrl = $null + $lastError = $null + foreach ($probe in '/wiki/api/v2/spaces', '/wiki/api/v2/pages') { + try { + $response = Invoke-ConfluenceRestMethod -ApiEndpoint $probe -Query @{ limit = 1 } -Context $Context + if ($response._links.base) { + $siteUrl = $response._links.base + break + } + } catch { + $lastError = $_ + continue + } + } + if (-not $siteUrl -and $lastError) { + Write-Verbose "Could not resolve the browsable site URL from the probe endpoints: $($lastError.Exception.Message)" + } + + [pscustomobject]@{ + CloudId = $cloudId + ApiBaseUri = $resolved.ApiBaseUri + SiteUrl = $siteUrl + } +} diff --git a/src/functions/public/Spaces/Get-ConfluenceSpace.ps1 b/src/functions/public/Spaces/Get-ConfluenceSpace.ps1 new file mode 100644 index 0000000..b3f27ad --- /dev/null +++ b/src/functions/public/Spaces/Get-ConfluenceSpace.ps1 @@ -0,0 +1,52 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceSpace { + <# + .SYNOPSIS + Get a Confluence space (read:space). + + .DESCRIPTION + Returns a space by key or by id. When neither is supplied the default + space key from the connected context is used. + + .EXAMPLE + ```powershell + Get-ConfluenceSpace -Key 'DOCS' + ``` + + Gets the space with key 'DOCS'. + + .LINK + https://psmodule.io/Confluence/Functions/Spaces/Get-ConfluenceSpace/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-space/ + #> + [CmdletBinding(DefaultParameterSetName = 'ByKey')] + param( + # The space key, e.g. 'DOCS'. + [Parameter(ParameterSetName = 'ByKey')] + [string]$Key, + + # The space id. + [Parameter(Mandatory, ParameterSetName = 'ById')] + [string]$Id, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + if ($PSCmdlet.ParameterSetName -eq 'ById') { + return Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/spaces/$Id" -Context $Context + } + + if ([string]::IsNullOrEmpty($Key)) { + $resolved = Resolve-ConfluenceContext -Context $Context + $Key = $resolved.SpaceKey + } + if ([string]::IsNullOrEmpty($Key)) { + throw 'Specify -Key or -Id, or connect with a default SpaceKey.' + } + + $response = Invoke-ConfluenceRestMethod -ApiEndpoint '/wiki/api/v2/spaces' -Query @{ keys = $Key } -Context $Context + $response.results | Where-Object { $_.key -eq $Key } | Select-Object -First 1 +} diff --git a/src/functions/public/Spaces/Get-ConfluenceSpacePermission.ps1 b/src/functions/public/Spaces/Get-ConfluenceSpacePermission.ps1 new file mode 100644 index 0000000..0914c85 --- /dev/null +++ b/src/functions/public/Spaces/Get-ConfluenceSpacePermission.ps1 @@ -0,0 +1,39 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceSpacePermission { + <# + .SYNOPSIS + List the permission assignments on a space (read:space). + + .DESCRIPTION + Returns every permission assignment in the space — each entry pairs a + principal (user or group) with an operation (for example read/space, + create/page, delete/page, restrict_content/space). This is the space's + whole permission table, not a filtered "effective permissions for the + current user" view (Confluence exposes that only on the v1 operations + expansion, which a granular v2 token cannot call). + + .EXAMPLE + ```powershell + Get-ConfluenceSpacePermission -SpaceId '123456' + ``` + + Lists the permission assignments in the space with ID 123456. + + .LINK + https://psmodule.io/Confluence/Functions/Spaces/Get-ConfluenceSpacePermission/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-space-permissions/ + #> + [CmdletBinding()] + param( + # The space id whose permission assignments are returned. + [Parameter(Mandatory)] + [string]$SpaceId, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/spaces/$SpaceId/permissions" -All -Context $Context +} diff --git a/src/functions/public/Spaces/Get-ConfluenceSpaceProperty.ps1 b/src/functions/public/Spaces/Get-ConfluenceSpaceProperty.ps1 new file mode 100644 index 0000000..8b35dd1 --- /dev/null +++ b/src/functions/public/Spaces/Get-ConfluenceSpaceProperty.ps1 @@ -0,0 +1,41 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceSpaceProperty { + <# + .SYNOPSIS + Get space properties (read:space). + + .DESCRIPTION + Returns all properties on a space, or the single property with the given key. + + .EXAMPLE + ```powershell + Get-ConfluenceSpaceProperty -SpaceId '123456' + ``` + + Gets all properties on the space with ID 123456. + + .LINK + https://psmodule.io/Confluence/Functions/Spaces/Get-ConfluenceSpaceProperty/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-space-properties/ + #> + [CmdletBinding()] + param( + # The space id. + [Parameter(Mandatory)] + [string]$SpaceId, + + # The property key to return. If omitted, all properties are returned. + [string]$Key, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + $all = Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/api/v2/spaces/$SpaceId/properties" -All -Context $Context + if ([string]::IsNullOrEmpty($Key)) { + return $all + } + $all | Where-Object { $_.key -eq $Key } | Select-Object -First 1 +} diff --git a/src/functions/public/Spaces/New-ConfluenceSpace.ps1 b/src/functions/public/Spaces/New-ConfluenceSpace.ps1 new file mode 100644 index 0000000..ea2d057 --- /dev/null +++ b/src/functions/public/Spaces/New-ConfluenceSpace.ps1 @@ -0,0 +1,62 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function New-ConfluenceSpace { + <# + .SYNOPSIS + Create a Confluence space (write:space). + + .DESCRIPTION + Creates a new global space with the given key and name, setting the + properties you supply. Fails if a space with that key already exists. + Space creation is + a v1-only operation (the v2 API exposes no space-write endpoint) reached + over the API gateway with the granular write:space scope; it also needs + the global "Create Spaces" permission on the account. The creating account + becomes an administrator of the new space. + + .EXAMPLE + ```powershell + New-ConfluenceSpace -Key 'DOCS' -Name 'Documentation' -Description 'Team docs' + ``` + + Creates a global space with key 'DOCS' named 'Documentation'. + + .LINK + https://psmodule.io/Confluence/Functions/Spaces/New-ConfluenceSpace/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-space/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The unique key for the new space (letters and numbers). + [Parameter(Mandatory)] + [string]$Key, + + # The display name of the new space. + [Parameter(Mandatory)] + [string]$Name, + + # An optional plain-text space description. + [string]$Description, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + $payload = @{ + key = $Key + name = $Name + } + if (-not [string]::IsNullOrEmpty($Description)) { + $payload['description'] = @{ + plain = @{ + value = $Description + representation = 'plain' + } + } + } + + if ($PSCmdlet.ShouldProcess($Key, 'Create Confluence space')) { + Invoke-ConfluenceRestMethod -ApiEndpoint '/wiki/rest/api/space' -Method 'POST' -Body $payload -Context $Context + } +} diff --git a/src/functions/public/Spaces/Remove-ConfluenceSpace.ps1 b/src/functions/public/Spaces/Remove-ConfluenceSpace.ps1 new file mode 100644 index 0000000..1b4f2a6 --- /dev/null +++ b/src/functions/public/Spaces/Remove-ConfluenceSpace.ps1 @@ -0,0 +1,42 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Remove-ConfluenceSpace { + <# + .SYNOPSIS + Permanently delete a Confluence space (delete:space and read:content.metadata). + + .DESCRIPTION + Permanently deletes a space and all of its content - the space is NOT sent + to the trash and cannot be restored. The call is asynchronous: Confluence + returns HTTP 202 with a long-running task descriptor. Deleting a space is a + v1-only operation reached over the API gateway; it needs the granular + delete:space scope (Atlassian also requires read:content.metadata on this + endpoint) plus Admin permission on the space. Because deletion is + irreversible, confirm the key belongs to a space you own before calling it. + + .EXAMPLE + ```powershell + Remove-ConfluenceSpace -Key 'DOCS' + ``` + + Permanently deletes the 'DOCS' space and returns the long-running task descriptor. + + .LINK + https://psmodule.io/Confluence/Functions/Spaces/Remove-ConfluenceSpace/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-space/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The key of the space to delete. + [Parameter(Mandatory)] + [string]$Key, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + if ($PSCmdlet.ShouldProcess($Key, 'Permanently delete Confluence space')) { + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/rest/api/space/$Key" -Method 'DELETE' -Context $Context + } +} diff --git a/src/functions/public/Spaces/Set-ConfluenceSpace.ps1 b/src/functions/public/Spaces/Set-ConfluenceSpace.ps1 new file mode 100644 index 0000000..db3dbf2 --- /dev/null +++ b/src/functions/public/Spaces/Set-ConfluenceSpace.ps1 @@ -0,0 +1,81 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Set-ConfluenceSpace { + <# + .SYNOPSIS + Create or declaratively configure a Confluence space (read:space and write:space). + + .DESCRIPTION + Ensures a space matches the supplied configuration (a declarative upsert). + If no space with the key exists it is created; if it exists it is updated. + Unlike Update-ConfluenceSpace, this is a full-state set: properties that are + not supplied are reset to their defaults (omitting -Description clears it). + To change individual fields while preserving the rest, use + Update-ConfluenceSpace. Space create/update are v1-only operations reached + over the API gateway; creating a space also needs the global "Create Spaces" + permission. + + .EXAMPLE + ```powershell + Set-ConfluenceSpace -Key 'DOCS' -Name 'Documentation' -Description 'Team docs' + ``` + + Creates the 'DOCS' space if it is missing, otherwise updates it, so its name + and description match exactly what was supplied. + + .EXAMPLE + ```powershell + Set-ConfluenceSpace -Key 'DOCS' -Name 'Documentation' + ``` + + Declaratively sets 'DOCS' with no description, clearing any existing description. + + .LINK + https://psmodule.io/Confluence/Functions/Spaces/Set-ConfluenceSpace/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-space/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The key of the space to create or configure. + [Parameter(Mandatory)] + [string]$Key, + + # The desired display name. A space always requires a name. + [Parameter(Mandatory)] + [string]$Name, + + # The desired plain-text description. Omit to clear the description. + [string]$Description, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + # Declarative: the supplied parameters are the full desired state, so an omitted + # -Description resets the description to empty rather than preserving it. + $payload = @{ + key = $Key + name = $Name + description = @{ + plain = @{ + value = [string]$Description + representation = 'plain' + } + } + } + + # Look the space up to choose between create (POST) and update (PUT). + $response = Invoke-ConfluenceRestMethod -ApiEndpoint '/wiki/api/v2/spaces' -Query @{ keys = $Key } -Context $Context + $existing = $response.results | Where-Object { $_.key -eq $Key } | Select-Object -First 1 + + if ($existing) { + if ($PSCmdlet.ShouldProcess($Key, 'Set Confluence space (update existing)')) { + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/rest/api/space/$Key" -Method 'PUT' -Body $payload -Context $Context + } + } else { + if ($PSCmdlet.ShouldProcess($Key, 'Set Confluence space (create)')) { + Invoke-ConfluenceRestMethod -ApiEndpoint '/wiki/rest/api/space' -Method 'POST' -Body $payload -Context $Context + } + } +} diff --git a/src/functions/public/Spaces/Update-ConfluenceSpace.ps1 b/src/functions/public/Spaces/Update-ConfluenceSpace.ps1 new file mode 100644 index 0000000..8e8709a --- /dev/null +++ b/src/functions/public/Spaces/Update-ConfluenceSpace.ps1 @@ -0,0 +1,70 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Update-ConfluenceSpace { + <# + .SYNOPSIS + Update a Confluence space's name or description (read:space and write:space). + + .DESCRIPTION + Updates one or more properties (name and/or description) on an existing + space. Only the fields you supply change; unspecified fields keep their + current values, because the space is read first (read:space) and then + written back (write:space). Fails if the space does not exist. For a + declarative create-or-replace, use Set-ConfluenceSpace instead. Updating a + space is a v1-only operation reached over the API gateway. + + .EXAMPLE + ```powershell + Update-ConfluenceSpace -Key 'DOCS' -Name 'Team Documentation' + ``` + + Renames the 'DOCS' space, keeping its existing description. + + .LINK + https://psmodule.io/Confluence/Functions/Spaces/Update-ConfluenceSpace/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-space/ + #> + [CmdletBinding(SupportsShouldProcess)] + param( + # The key of the space to update. + [Parameter(Mandatory)] + [string]$Key, + + # The new display name. Defaults to the existing name. + [string]$Name, + + # The new plain-text description. Defaults to the existing description. + [string]$Description, + + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + # Read the current space (with its plain-text description) so a caller who + # updates only one field does not blank the other on the full-body v1 PUT. + $query = @{ keys = $Key; 'description-format' = 'plain' } + $response = Invoke-ConfluenceRestMethod -ApiEndpoint '/wiki/api/v2/spaces' -Query $query -Context $Context + $current = $response.results | Where-Object { $_.key -eq $Key } | Select-Object -First 1 + if (-not $current) { + throw "Confluence space '$Key' was not found." + } + + $newName = if ($PSBoundParameters.ContainsKey('Name')) { $Name } else { $current.name } + $newDescription = if ($PSBoundParameters.ContainsKey('Description')) { $Description } else { [string]$current.description.plain.value } + + $payload = @{ + key = $Key + name = $newName + description = @{ + plain = @{ + value = $newDescription + representation = 'plain' + } + } + } + + if ($PSCmdlet.ShouldProcess($Key, 'Update Confluence space')) { + Invoke-ConfluenceRestMethod -ApiEndpoint "/wiki/rest/api/space/$Key" -Method 'PUT' -Body $payload -Context $Context + } +} diff --git a/src/functions/public/Users/Get-ConfluenceCurrentUser.ps1 b/src/functions/public/Users/Get-ConfluenceCurrentUser.ps1 new file mode 100644 index 0000000..d3c2544 --- /dev/null +++ b/src/functions/public/Users/Get-ConfluenceCurrentUser.ps1 @@ -0,0 +1,34 @@ +#SkipTest:FunctionTest:Integration tests are added once the repository Confluence credentials are configured. +function Get-ConfluenceCurrentUser { + <# + .SYNOPSIS + Get the current (authenticated) user — Confluence's "me" endpoint (read:content-details). + + .DESCRIPTION + Returns the account behind the connected token via the Confluence + current-user endpoint (v1 /wiki/rest/api/user/current), which requires + read:content-details. The read:user scope only covers the anonymous-user + and group-membership endpoints, so a token without read:content-details is + rejected with a scope-check 401. + + .EXAMPLE + ```powershell + Get-ConfluenceCurrentUser + ``` + + Returns the account behind the connected token. + + .LINK + https://psmodule.io/Confluence/Functions/Users/Get-ConfluenceCurrentUser/ + + .LINK + https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-users/ + #> + [CmdletBinding()] + param( + # The context to use: an object, a context name, or $null for the default. + [object]$Context + ) + + Invoke-ConfluenceRestMethod -ApiEndpoint '/wiki/rest/api/user/current' -Context $Context +} diff --git a/src/header.ps1 b/src/header.ps1 deleted file mode 100644 index cc1fde9..0000000 --- a/src/header.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')] -[CmdletBinding()] -param() diff --git a/src/init/initializer.ps1 b/src/init/initializer.ps1 deleted file mode 100644 index 28396fb..0000000 --- a/src/init/initializer.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Write-Verbose '-------------------------------' -Write-Verbose '--- THIS IS AN INITIALIZER ---' -Write-Verbose '-------------------------------' diff --git a/src/scripts/loader.ps1 b/src/scripts/loader.ps1 deleted file mode 100644 index 973735a..0000000 --- a/src/scripts/loader.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -Write-Verbose '-------------------------' -Write-Verbose '--- THIS IS A LOADER ---' -Write-Verbose '-------------------------' diff --git a/src/variables/private/Confluence.ps1 b/src/variables/private/Confluence.ps1 new file mode 100644 index 0000000..fc10b32 --- /dev/null +++ b/src/variables/private/Confluence.ps1 @@ -0,0 +1,17 @@ +$script:Confluence = [pscustomobject]@{ + ContextVault = 'PSModule.Confluence' + # The API-gateway base is https://api.atlassian.com/ex/confluence/