IchicraftWidgets.psm1
Add-Type -AssemblyName System.IO.Compression.FileSystem function Zip { param([string]$filePath, [string]$relPath, [string]$outZip) [System.IO.Compression.CompressionLevel]$compression = "Optimal" $ziparchive = [System.IO.Compression.ZipFile]::Open( $outZip, "Update" ) Write-Host "Zip $file to $outZip" -Fore Yellow $relPath = $relPath.Replace("\", "/"); $entry = $ziparchive.Entries | ? { $_.FullName -eq $relPath }; if ($entry -ne $null) { $entry.Delete(); } # add file [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($ziparchive, $filePath, $relPath, $compression) | Out-Null $ziparchive.Dispose() } function validateConnection([SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]$connection, [SharePointPnP.PowerShell.Commands.Enums.ConnectionType]$type) { if ($null -eq $connection) { throw "No SPOnlineConnection is available, connect to the target site with the Connect-PnPOnline Cmdlet" return $false } if ($connection.ConnectionType -ne $type) { if ($type -eq [SharePointPnP.PowerShell.Commands.Enums.ConnectionType]::TenantAdmin) { Write-Error "Current SPOnlineConnection is not a valid connection (expected: $type, actual: $($connection.ConnectionType)), connect to the tenant admin site with the Connect-PnPOnline Cmdlet" } elseif ($type -eq [SharePointPnP.PowerShell.Commands.Enums.ConnectionType]::O365) { Write-Error "Current SPOnlineConnection is not a valid connection (expected: $type, actual: $($connection.ConnectionType)), connect to the site-collection with the Connect-PnPOnline Cmdlet" } return $false } return $true } function validateConnection([SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]$connection) { if ($null -eq $connection) { throw "No SPOnlineConnection is available, connect to the target site with the Connect-PnPOnline Cmdlet" return $false } return $true } function Add-IchicraftWidgetsApp { [CmdletBinding()] param( [parameter( Mandatory = $false, HelpMessage = "PnP connection. Depending on the connection.ConnectionType property, the SPPKG will be deployed to either the site- or tenant application catalog (connection.ConnectionType: 'TenantAdmin' = tenant, 'O365' = site)")] [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection] $connection, [parameter( Mandatory = $false, HelpMessage = "Temporary location to download the SPPKG package, defaults to {userprofile}\downloads")] [string] $tempLocation, [parameter( Mandatory = $false, HelpMessage = "Only applicable if you host your own WidgetBoard service back-end, leave empty for a default installation")] [string] $customServiceHostName ) begin { if ($null -eq $connection) { $connection = Get-PnPConnection } [bool]$isValidConnection = validateConnection -connection $connection if (!$isValidConnection) { break } if ($tempLocation.Length -eq 0) { $tempLocation = "$($env:userprofile)\downloads" } else { $tempLocation = $tempLocation.TrimEnd('\') } } process { if (Test-Path "$($tempLocation)\ichicraft-widgetboard.sppkg") { Write-Host "Removing existing SPPKG package before download" Remove-Item "$($tempLocation)\ichicraft-widgetboard.sppkg" -Force -Confirm:$false } # Download the Widgets SPPKG package Write-Host "Start package download" Start-BitsTransfer -Source https://ichicraft.blob.core.windows.net/widgetboard/public/0.2.9/ichicraft-widgetboard.sppkg -Destination $tempLocation if ($customServiceHostName) { if (Test-Path "$($tempLocation)\ichicraft-widgetboard.zip") { Write-Host "Removing existing SPPKG zip file" Remove-Item "$($tempLocation)\ichicraft-widgetboard.zip" -Force -Confirm:$false } # rename package Write-Host "Renaming .SPPKG package to .zip file" Rename-Item -Path "$($tempLocation)\ichicraft-widgetboard.sppkg" -NewName "$($tempLocation)\ichicraft-widgetboard.zip" -Force -Confirm:$false if (Test-Path "$($tempLocation)\ichicraft-widgetboard") { Write-Host "Removing existing expanded archive folder" Remove-Item "$($tempLocation)\ichicraft-widgetboard" -Force -Confirm:$false -Recurse } # extract package to temp location Write-Host "Unpacking SPPKG zip file" Expand-Archive -LiteralPath "$($tempLocation)\ichicraft-widgetboard.zip" -DestinationPath "$($tempLocation)\ichicraft-widgetboard" Write-Host "Removing SPPKG zip file" # Remove-Item "$($tempLocation)\ichicraft-widgetboard.zip" -Force -Confirm:$false -Recurse try { $defaultHostName = 'ichicraft-widgetboard.azurewebsites.net'; Write-Host "Updating package with custom service host name" # replace default service host name with custom service host name Get-ChildItem -Path "$($tempLocation)\ichicraft-widgetboard" -Include WebPart_*.xml -recurse | ForEach-Object { $file = $_ [string]$xml = Get-Content $_ if ($xml.Contains($defaultHostName)) { $xml = $xml.Replace($defaultHostName, $customServiceHostName) Write-Host "Updating file contents $_" Set-Content -Path $_ -Value $xml -Force -Confirm:$false Write-host "Updating file in zip" $relPath = $file.FullName.Replace("$($tempLocation)\ichicraft-widgetboard\", "") Zip -filePath $file.FullName -relPath $relPath -outZip "$($tempLocation)\ichicraft-widgetboard.zip" } } # rename package Rename-Item -Path "$($tempLocation)\ichicraft-widgetboard.zip" -NewName "$($tempLocation)\ichicraft-widgetboard.sppkg" } catch { Write-Error $_ throw; } finally { # remove temp directory Remove-Item -LiteralPath "$($tempLocation)\ichicraft-widgetboard" -Recurse -Confirm:$false -Force } } if ($connection.ConnectionType -eq [SharePointPnP.PowerShell.Commands.Enums.ConnectionType]::TenantAdmin) { # Add the package to the sites app catalog Write-Host "Start adding the Widgets app to the tenant app catalog" Retry-Command -ScriptBlock { Add-PnPApp -Path "$($tempLocation)\ichicraft-widgetboard.sppkg" -Scope Tenant -Publish -Overwrite -ErrorAction Stop } -Verbose } if ($connection.ConnectionType -eq [SharePointPnP.PowerShell.Commands.Enums.ConnectionType]::O365) { # Add the package to the sites app catalog Write-Host "Start adding the Widgets app to the site app catalog" Retry-Command -ScriptBlock { Add-PnPApp -Path "$($tempLocation)\ichicraft-widgetboard.sppkg" -Scope Site -Publish -Overwrite -ErrorAction Stop } -Verbose } # remove the download # Remove-Item -LiteralPath "$($tempLocation)\ichicraft-widgetboard.sppkg" -Force -Confirm:$false } } function Approve-IchicraftWidgetsTenantServicePrincipalPermissionRequests { [CmdletBinding()] param( [parameter( Mandatory = $false, HelpMessage = "Connection with permissions to approve the TenantServicePrincipalPermissionRequest (Approve-PnPTenantServicePrincipalPermissionRequest)")] [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection] $connection ) begin { if ($null -eq $connection) { $connection = Get-PnPConnection } [bool]$isValidConnection = validateConnection -connection $connection -type TenantAdmin if (!$isValidConnection) { break } } process { # Approve the API permission requests Write-Host "Start approving the API permission requests" Retry-Command -ScriptBlock { Get-PnPTenantServicePrincipalPermissionRequests | Where-Object { $_.PackageName -eq "Ichicraft-WidgetBoardWebpart" } | ForEach-Object { Approve-PnPTenantServicePrincipalPermissionRequest -RequestId $_.Id -Force -ErrorAction SilentlyContinue } } -Verbose } } function Add-IchicraftWidgetsClientSideWebPart { [CmdletBinding()] param( [parameter( Mandatory = $false, HelpMessage = "Connection to the site where the webpart should be added")] [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection] $connection ) begin { if ($null -eq $connection) { $connection = Get-PnPConnection } [bool]$isValidConnection = validateConnection -connection $connection -type O365 if (!$isValidConnection) { break } } process { $siteUrl = $connection.Url $widgetsApp = Get-PnPApp -Identity "Ichicraft-WidgetBoardWebpart" -Scope Tenant -ErrorAction SilentlyContinue if ($null -eq $widgetsApp) { throw "The Ichicraft-WidgetBoardWebpart app is not found in the tenant Application Catalog. Please run Add-IchicraftWidgetsApp first." } # Install the package to the site Write-Host "Start installing the Widgets app to $siteUrl" Retry-Command -ScriptBlock { Install-PnPApp -Identity "Ichicraft-WidgetBoardWebpart" -Scope Tenant -ErrorAction Stop } -Verbose # Add a full-with section to the page Write-Host "Start adding a full-width section to the homepage of $siteUrl" Retry-Command -ScriptBlock { Add-PnPClientSidePageSection -Page "home" -SectionTemplate OneColumnFullWidth -Order 0 } -Verbose # Add the Widgets webpart to the page Write-Host "Start adding the Widgets webpart" Retry-Command -ScriptBlock { Add-PnPClientSideWebPart -Page "home" -Component "Widget Board" -Section 1 -Column 1 -ErrorAction Stop } -Verbose write-host "Finished, now try and open $siteUrl to configure your widgetboard" } } function Export-IchicraftWidgetsConfiguration { [CmdletBinding()] # This script ensures the lists needed by the Ichicraft Widgets webpart param( [parameter( Mandatory = $false, HelpMessage = "Connection to the site from which to export the configuration")] [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection] $connection, $outputFilePath = "./adminConfig.json" ) begin { if ($null -eq $connection) { $connection = Get-PnPConnection } [bool]$isValidConnection = validateConnection -connection $connection -type O365 if (!$isValidConnection) { break } } process { $siteUrl = $connection.Url $adminConfigItem = Get-PnPListItem -List "WidgetBoard_AdminConfig" -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $adminConfigItem) { throw "No configuration found in site $siteUrl" } $config = $adminConfigItem["Config"]; $config | Out-File -Encoding utf8 -Force -FilePath $outputFilePath -Append:$false } } function CreateConfigList($name, $connection) { $list = Get-PnPList -Identity $name -ErrorAction:SilentlyContinue -Connection $connection if (-not $list) { New-PnPList -Title $name -Template GenericList -Hidden -EnableContentTypes:$false -EnableVersioning:$true -OnQuickLaunch:$false -Connection $connection } Set-PnPList -Identity $name -Description 'Used by ichicraft widgets' -EnableAttachments:$false -Connection $connection # Set NoCrawl $list = Get-PnPList -Identity $name -Connection $connection $list.NoCrawl = $true $list.Update() $list.Context.ExecuteQuery() if (-not (Get-PnPField -List "WidgetBoard_UserConfig" -Identity "Config" -ErrorAction SilentlyContinue -Connection $connection)) { # Add the config field Add-PnPField -List $name -DisplayName "Config" -InternalName "Config" -Type Note -AddToDefaultView -Connection $connection } } function Initialize-IchicraftWidgetsWebpart { [CmdletBinding()] # This script ensures the lists needed by the Ichicraft Widgets webpart param( [parameter( Mandatory = $false, HelpMessage = "Connection to the site with the webpart")] [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection] $connection, $configFilePath = "./adminConfig.json" ) begin { if ($null -eq $connection) { $connection = Get-PnPConnection } [bool]$isValidConnection = validateConnection -connection $connection -type O365 if (!$isValidConnection) { break } } process { $siteUrl = $connection.Url write-host $siteUrl # create adminconfig list CreateConfigList -name "WidgetBoard_AdminConfig" $adminConfig = Get-Content -Path $configFilePath -Raw -Encoding UTF8 $item = Get-PnPListItem -List "WidgetBoard_AdminConfig" | Select-Object -First 1 if ($item) { write-host "Updating admin config list item" $item = Set-PnPListItem -Identity $item.Id -List "WidgetBoard_AdminConfig" -Values @{"Title" = "Config"; "Config" = $adminConfig } } else { write-host "Adding admin config list item" $item = Add-PnPListItem -List "WidgetBoard_AdminConfig" -Values @{"Title" = "Config"; "Config" = $adminConfig } } # create userconfig list CreateConfigList -name "WidgetBoard_UserConfig" $list = Get-PnPList -Identity "WidgetBoard_UserConfig" $list.ReadSecurity = 2 $list.WriteSecurity = 2 $list.Context.ExecuteQuery() # create assets list $list = Get-PnPList -Identity "WidgetBoard_Assets" -ErrorAction:SilentlyContinue if (-not $list) { New-PnPList -Title "WidgetBoard_Assets" -Template DocumentLibrary -Hidden -EnableContentTypes:$false -EnableVersioning:$false -OnQuickLaunch:$false -ErrorAction SilentlyContinue } # Tell the service the board has been installed [int]$rand = Get-Random -Maximum 10000 -Minimum 1000 Invoke-RestMethod -Method Post -Uri "https://ichicraft-widgetboard.azurewebsites.net/config/update/etag/$($rand)?referrer=$($siteUrl)" } } function Retry-Command { [CmdletBinding()] param ( [parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [scriptblock] $ScriptBlock, [int] $RetryCount = 100, [int] $TimeoutInSecs = 5, [string] $description = "" ) process { $Attempt = 1 $Flag = $true Write-Host "[START] $description" do { try { $PreviousPreference = $ErrorActionPreference $ErrorActionPreference = 'Stop' Invoke-Command -ScriptBlock $ScriptBlock -OutVariable Result $ErrorActionPreference = $PreviousPreference # flow control will execute the next line only if the command in the scriptblock executed without any errors # if an error is thrown, flow control will go to the 'catch' block Write-Host "[FINISHED] $description `n" $Flag = $false } catch { if ($Attempt -gt $RetryCount) { Write-Host "[FAILED] $description! Total retry attempts: $RetryCount" $Flag = $false } else { Write-Verbose "[Error Message] $($_.exception.message) `n" Write-Verbose "[$Attempt/$RetryCount] $description. Retrying in $TimeoutInSecs seconds..." Start-Sleep -Seconds $TimeoutInSecs $Attempt = $Attempt + 1 } } } While ($Flag) } } Export-ModuleMember -Function Add-IchicraftWidgetsApp, Add-IchicraftWidgetsClientSideWebPart, Approve-IchicraftWidgetsTenantServicePrincipalPermissionRequests, Initialize-IchicraftWidgetsWebpart, Export-IchicraftWidgetsConfiguration; |