├── .vscode └── settings.json ├── ACL-Permissions ├── ACL-Permissions.psd1 └── ACL-Permissions.psm1 ├── Add-HostFileEntry ├── Add-HostFileEntry.Tests.ps1 ├── Add-HostFileEntry.psd1 ├── Add-HostFileEntry.psm1 └── fixtures │ ├── empty.hosts │ ├── no-blank-at-end.hosts │ └── three-hosts.hosts ├── AdministratorRole ├── AdministratorRole.psd1 └── AdministratorRole.psm1 ├── BindingRedirects ├── BindingRedirects.psd1 └── BindingRedirects.psm1 ├── CHANGES.md ├── DnnWebsiteManagement ├── DnnWebsiteManagement.psd1 └── DnnWebsiteManagement.psm1 ├── LICENSE ├── PSScriptAnalyzerSettings.psd1 ├── README.md ├── Read-Choice ├── Read-Choice.psd1 └── Read-Choice.psm1 ├── Recycle ├── Recycle.psd1 └── Recycle.psm1 ├── SslWebBinding ├── SslWebBinding.psd1 └── SslWebBinding.psm1 ├── Write-HtmlNode ├── Write-HtmlNode.psd1 └── Write-HtmlNode.psm1 └── azure-pipelines.yml /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "powershell.scriptAnalysis.settingsPath": "./PSScriptAnalyzerSettings.psd1" 3 | } -------------------------------------------------------------------------------- /ACL-Permissions/ACL-Permissions.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'ACL-Permissions' 3 | # 4 | # Generated by: Brian Dukes 5 | # 6 | # Generated on: 10/10/2016 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'ACL-Permissions.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.1.0' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '908787a0-5a50-43c8-816a-7fa411b4e562' 22 | 23 | # Author of this module 24 | Author = 'Brian Dukes' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Engage Software' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2022 Engage Software' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'A couple of ACL utilities, for repairing corrupt permissions and applying permissions for IIS AppPool identities' 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | # PowerShellVersion = '' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | RequiredModules = @( @{ ModuleName = 'AdministratorRole'; ModuleVersion = '1.0.1'; GUID = '694c2097-6b13-4735-8d6e-396224d646cc' }, 55 | @{ ModuleName = 'IISAdministration'; ModuleVersion = '1.1.0.0' } ) 56 | 57 | # Assemblies that must be loaded prior to importing this module 58 | # RequiredAssemblies = @() 59 | 60 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 61 | # ScriptsToProcess = @() 62 | 63 | # Type files (.ps1xml) to be loaded when importing this module 64 | # TypesToProcess = @() 65 | 66 | # Format files (.ps1xml) to be loaded when importing this module 67 | # FormatsToProcess = @() 68 | 69 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 70 | # NestedModules = @() 71 | 72 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 73 | FunctionsToExport = @('Set-ModifyPermission', 'Repair-AclCorruption') 74 | 75 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 76 | CmdletsToExport = @() 77 | 78 | # Variables to export from this module 79 | VariablesToExport = @() 80 | 81 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 82 | AliasesToExport = @() 83 | 84 | # DSC resources to export from this module 85 | # DscResourcesToExport = @() 86 | 87 | # List of all modules packaged with this module 88 | # ModuleList = @() 89 | 90 | # List of all files packaged with this module 91 | FileList = 'ACL-Permissions.psm1' 92 | 93 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 94 | PrivateData = @{ 95 | 96 | PSData = @{ 97 | 98 | # Tags applied to this module. These help with module discovery in online galleries. 99 | Tags = @('PSEdition_Core', 'PSEdition_Desktop', 'Windows') 100 | 101 | # A URL to the license for this module. 102 | LicenseUri = 'https://github.com/bdukes/PowerShellModules/blob/main/LICENSE' 103 | 104 | # A URL to the main website for this project. 105 | ProjectUri = 'https://github.com/bdukes/PowerShellModules' 106 | 107 | # A URL to an icon representing this module. 108 | # IconUri = '' 109 | 110 | # ReleaseNotes of this module 111 | ReleaseNotes = 'https://github.com/bdukes/PowerShellModules/blob/main/CHANGES.md' 112 | 113 | } # End of PSData hashtable 114 | 115 | } # End of PrivateData hashtable 116 | 117 | # HelpInfo URI of this module 118 | # HelpInfoURI = '' 119 | 120 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 121 | # DefaultCommandPrefix = '' 122 | 123 | } 124 | 125 | -------------------------------------------------------------------------------- /ACL-Permissions/ACL-Permissions.psm1: -------------------------------------------------------------------------------- 1 | #Requires -Modules AdministratorRole 2 | Set-StrictMode -Version Latest 3 | 4 | function Repair-AclCorruption { 5 | param( 6 | [parameter(Mandatory = $true, position = 0)]$directory); 7 | 8 | $out = icacls "$directory" /verify /t /q 9 | 10 | foreach ($line in $out) { 11 | if ($line -match '(.:[^:]*): (.*)') { 12 | $path = $Matches[1] 13 | Set-Acl $path (Get-Acl $path) 14 | } 15 | } 16 | <# 17 | .SYNOPSIS 18 | Fixes ACLs on the directory (and its ancestors) that have become corrupted 19 | .DESCRIPTION 20 | When the error message "This access control list is not in canonical form and therefore cannot be modified." comes up, you can use this to fix the ACLs 21 | Based on https://gist.github.com/vbfox/8fbec5c60b0c16289023, found from http://serverfault.com/a/287702/4110 22 | .PARAMETER directory 23 | The path to the directory to which to apply permissions 24 | .PARAMETER username 25 | The username of the account to which to give permissions 26 | .PARAMETER domain 27 | The domain of the account to which to give permissions, defaults to the App Pool Identity domain 28 | #> 29 | } 30 | 31 | function Set-ModifyPermission { 32 | [CmdletBinding(SupportsShouldProcess)] 33 | param( 34 | [parameter(Mandatory = $true, position = 0)]$directory, 35 | [parameter(Mandatory = $true, position = 1)]$username, 36 | $domain = 'IIS APPPOOL'); 37 | 38 | Assert-AdministratorRole 39 | 40 | $inherit = [system.security.accesscontrol.InheritanceFlags]"ContainerInherit, ObjectInherit" 41 | $propagation = [system.security.accesscontrol.PropagationFlags]"None" 42 | 43 | if ($domain -eq 'IIS APPPOOL') { 44 | Import-Module IisAdministration; 45 | $serverManager = Get-IISServerManager; 46 | $appPool = $serverManager.APplicationPools[$username]; 47 | $sid = $appPool.attributes['applicationPoolSid'].Value; 48 | $identifier = New-Object System.Security.Principal.SecurityIdentifier($sid) 49 | $user = $identifier.Translate([System.Security.Principal.NTAccount]) 50 | } 51 | else { 52 | $user = New-Object System.Security.Principal.NTAccount($domain, $username) 53 | } 54 | 55 | $accessrule = New-Object system.security.AccessControl.FileSystemAccessRule($user, "Modify", $inherit, $propagation, "Allow") 56 | 57 | if ($PSCmdlet.ShouldProcess($directory)) { 58 | Repair-AclCorruption $directory 59 | $acl = Get-Acl $directory 60 | $acl.AddAccessRule($accessrule) 61 | set-acl -aclobject $acl $directory 62 | } 63 | <# 64 | .SYNOPSIS 65 | Gives the given user the modify permission to the given directory 66 | .PARAMETER directory 67 | The path to the directory to which to apply permissions 68 | .PARAMETER username 69 | The username of the account to which to give permissions 70 | .PARAMETER domain 71 | The domain of the account to which to give permissions, defaults to the App Pool Identity domain 72 | #> 73 | } 74 | 75 | Export-ModuleMember Set-ModifyPermission 76 | Export-ModuleMember Repair-AclCorruption 77 | -------------------------------------------------------------------------------- /Add-HostFileEntry/Add-HostFileEntry.Tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PsScriptRoot\..\AdministratorRole\AdministratorRole.psd1 2 | Import-Module $PsScriptRoot\Add-HostFileEntry.psd1 3 | 4 | Describe 'Add-HostFileEntry' { 5 | BeforeEach { $env:windir = 'TestDrive:' } 6 | 7 | Context 'No blank line at end of hosts file' { 8 | 9 | Mock -ModuleName Add-HostFileEntry Assert-AdministratorRole {} 10 | mkdir TestDrive:\System32\drivers\etc 11 | Copy-Item $PsScriptRoot\fixtures\no-blank-at-end.hosts TestDrive:\System32\drivers\etc\hosts 12 | 13 | Add-HostFileEntry test.test 14 | 15 | It 'Adds host file entry' { 16 | 'TestDrive:\System32\drivers\etc\hosts' | Should -FileContentMatch '^127\.0\.0\.1\s+test\.test$' 17 | } 18 | } 19 | 20 | Context 'Empty hosts file' { 21 | 22 | Mock -ModuleName Add-HostFileEntry Assert-AdministratorRole {} 23 | mkdir TestDrive:\System32\drivers\etc 24 | Copy-Item $PsScriptRoot\fixtures\empty.hosts TestDrive:\System32\drivers\etc\hosts 25 | 26 | Add-HostFileEntry test.test 27 | 28 | It 'Adds host file entry' { 29 | 'TestDrive:\System32\drivers\etc\hosts' | Should -FileContentMatch '^127\.0\.0\.1\s+test\.test$' 30 | } 31 | } 32 | } 33 | 34 | Describe 'Remove-HostFileEntry' { 35 | BeforeEach { $env:windir = 'TestDrive:' } 36 | 37 | Context 'No blank line at end of hosts file' { 38 | 39 | Mock -ModuleName Add-HostFileEntry Assert-AdministratorRole {} 40 | mkdir TestDrive:\System32\drivers\etc 41 | Copy-Item $PsScriptRoot\fixtures\no-blank-at-end.hosts TestDrive:\System32\drivers\etc\hosts 42 | 43 | Remove-HostFileEntry example.example 44 | 45 | It 'Removes host file entry' { 46 | 'TestDrive:\System32\drivers\etc\hosts' | Should -Not -FileContentMatch '^127\.0\.0\.1\s+example\.example$' 47 | } 48 | } 49 | 50 | Context 'Empty hosts file' { 51 | 52 | Mock -ModuleName Add-HostFileEntry Assert-AdministratorRole {} 53 | mkdir TestDrive:\System32\drivers\etc 54 | Copy-Item $PsScriptRoot\fixtures\empty.hosts TestDrive:\System32\drivers\etc\hosts 55 | 56 | Remove-HostFileEntry test.test 57 | 58 | It 'Leaves empty host file' { 59 | 'TestDrive:\System32\drivers\etc\hosts' | Should -Not -FileContentMatchMultiline '.*' 60 | } 61 | } 62 | 63 | Context 'Three Hosts' { 64 | 65 | Mock -ModuleName Add-HostFileEntry Assert-AdministratorRole {} 66 | mkdir TestDrive:\System32\drivers\etc 67 | Copy-Item $PsScriptRoot\fixtures\three-hosts.hosts TestDrive:\System32\drivers\etc\hosts 68 | 69 | Remove-HostFileEntry two.example 70 | 71 | It 'Leaves two hosts' { 72 | 'TestDrive:\System32\drivers\etc\hosts' | Should -Not -FileContentMatch '^127\.0\.0\.1\s+two\.example$' 73 | 'TestDrive:\System32\drivers\etc\hosts' | Should -FileContentMatch '^127\.0\.0\.1\s+one\.example$' 74 | 'TestDrive:\System32\drivers\etc\hosts' | Should -FileContentMatch '^127\.0\.0\.1\s+three\.example$' 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Add-HostFileEntry/Add-HostFileEntry.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'Add-HostFileEntry' 3 | # 4 | # Generated by: Brian Dukes 5 | # 6 | # Generated on: 10/10/2016 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'Add-HostFileEntry.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.1.0' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '16e30c8c-8de5-4090-a542-e8f9594ca613' 22 | 23 | # Author of this module 24 | Author = 'Brian Dukes' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Engage Software' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2022 Engage Software' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Adds and removes entries from the HOSTS file' 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | # PowerShellVersion = '' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | RequiredModules = @( @{ ModuleName = 'AdministratorRole'; ModuleVersion = '1.0.1'; GUID = '694c2097-6b13-4735-8d6e-396224d646cc' } ) 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = @('Add-HostFileEntry', 'Remove-HostFileEntry') 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = @() 76 | 77 | # Variables to export from this module 78 | VariablesToExport = @() 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = @() 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | FileList = 'Add-HostFileEntry.psm1' 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | Tags = @('PSEdition_Core', 'PSEdition_Desktop', 'Windows') 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'https://github.com/bdukes/PowerShellModules/blob/main/LICENSE' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/bdukes/PowerShellModules' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | ReleaseNotes = 'https://github.com/bdukes/PowerShellModules/blob/main/CHANGES.md' 111 | 112 | } # End of PSData hashtable 113 | 114 | } # End of PrivateData hashtable 115 | 116 | # HelpInfo URI of this module 117 | # HelpInfoURI = '' 118 | 119 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 120 | # DefaultCommandPrefix = '' 121 | 122 | } 123 | 124 | -------------------------------------------------------------------------------- /Add-HostFileEntry/Add-HostFileEntry.psm1: -------------------------------------------------------------------------------- 1 | #Requires -Version 3 2 | #Requires -Modules AdministratorRole 3 | Set-StrictMode -Version:Latest 4 | 5 | function Add-HostFileEntry { 6 | [CmdletBinding(SupportsShouldProcess)] 7 | param( 8 | [parameter(Mandatory = $true, position = 0)] 9 | [string]$hostName, 10 | [string]$ipAddress = '127.0.0.1' 11 | ); 12 | Assert-AdministratorRole; 13 | 14 | $hostsLocation = "$env:windir\System32\drivers\etc\hosts"; 15 | $hostsContent = Get-Content -Path $hostsLocation -Raw; 16 | 17 | $ipRegex = [regex]::Escape($ipAddress); 18 | $hostRegex = [regex]::Escape($hostName); 19 | 20 | $existingEntry = $hostsContent -match "(?:`n|\A)\s*$ipRegex\s+$hostRegex\s*(?:`n|\Z)"; 21 | if (-not $existingEntry) { 22 | if ($hostsContent -notmatch "`n\s*$") { 23 | # Add line break if missing from last line 24 | if ($PSCmdlet.ShouldProcess($hostsLocation, 'Add Blank Line')) { 25 | Add-Content -Path $hostsLocation -Value ''; 26 | } 27 | } 28 | 29 | if ($PSCmdlet.ShouldProcess($hostsLocation, "Add entry mapping $hostName to $ipAddress")) { 30 | Add-Content -Path $hostsLocation -Value "$ipAddress`t`t$hostName"; 31 | } 32 | } 33 | else { 34 | Write-Verbose -Message "Entry mapping $hostName to $ipAddress already exists in $hostsLocation"; 35 | } 36 | <# 37 | .SYNOPSIS 38 | Adds an entry to the HOSTS file 39 | .DESCRIPTION 40 | If it doesn't already exist, adds a line to the HOSTS file mapping the given host name to the given IP address 41 | .PARAMETER hostName 42 | The host name to map 43 | .PARAMETER ipAddress 44 | The IP address to map 45 | #> 46 | } 47 | 48 | function Remove-HostFileEntry { 49 | [CmdletBinding(SupportsShouldProcess)] 50 | param( 51 | [parameter(Mandatory = $true, position = 0)] 52 | [string]$hostName, 53 | [string]$ipAddress = '127.0.0.1' 54 | ); 55 | Assert-AdministratorRole; 56 | 57 | $hostsLocation = "$env:windir\System32\drivers\etc\hosts"; 58 | 59 | $ipRegex = [regex]::Escape($ipAddress); 60 | $hostRegex = [regex]::Escape($hostName); 61 | 62 | $oldHostsContent = Get-Content -Path $hostsLocation -Raw 63 | $newHostsFileContent = $oldHostsContent | ForEach-Object { $_ -replace "(?:`n|\A)\s*$ipRegex\s+$hostRegex\s*(?:`n|\Z)", "`n" }; 64 | 65 | if ($PSCmdlet.ShouldProcess($hostsLocation, "Remove entry mapping $hostName to $ipAddress")) { 66 | [System.IO.File]::WriteAllText($hostsLocation, $newHostsFileContent); 67 | } 68 | <# 69 | .SYNOPSIS 70 | Removes an entry from the HOSTS file 71 | .DESCRIPTION 72 | Updates the HOSTS file to remove a line mapping the given host name to the given IP address 73 | .PARAMETER hostName 74 | The host name to remove 75 | .PARAMETER ipAddress 76 | The IP address to remove 77 | #> 78 | } 79 | 80 | Export-ModuleMember -Function Add-HostFileEntry; 81 | Export-ModuleMember -Function Remove-HostFileEntry; 82 | -------------------------------------------------------------------------------- /Add-HostFileEntry/fixtures/empty.hosts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdukes/PowerShellModules/3266346fca63bd3fe568c626ef5cbe3c465b9c89/Add-HostFileEntry/fixtures/empty.hosts -------------------------------------------------------------------------------- /Add-HostFileEntry/fixtures/no-blank-at-end.hosts: -------------------------------------------------------------------------------- 1 | 127.0.0.1 example.example -------------------------------------------------------------------------------- /Add-HostFileEntry/fixtures/three-hosts.hosts: -------------------------------------------------------------------------------- 1 | 127.0.0.1 one.example 2 | 127.0.0.1 two.example 3 | 127.0.0.1 three.example 4 | -------------------------------------------------------------------------------- /AdministratorRole/AdministratorRole.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'AdministratorRole' 3 | # 4 | # Generated by: Brian Dukes 5 | # 6 | # Generated on: 10/10/2016 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'AdministratorRole.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.1.0' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '694c2097-6b13-4735-8d6e-396224d646cc' 22 | 23 | # Author of this module 24 | Author = 'Brian Dukes' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Engage Software' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2022 Engage Software' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Utilities for determining whether the current user is an administrator' 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | # PowerShellVersion = '' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = @('Test-AdministratorRole', 'Assert-AdministratorRole', 'Invoke-Elevated') 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = @() 76 | 77 | # Variables to export from this module 78 | VariablesToExport = @() 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = @() 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | FileList = 'AdministratorRole.psm1' 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | Tags = @('PSEdition_Core', 'PSEdition_Desktop', 'Windows') 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'https://github.com/bdukes/PowerShellModules/blob/main/LICENSE' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/bdukes/PowerShellModules' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | ReleaseNotes = 'https://github.com/bdukes/PowerShellModules/blob/main/CHANGES.md' 111 | 112 | } # End of PSData hashtable 113 | 114 | } # End of PrivateData hashtable 115 | 116 | # HelpInfo URI of this module 117 | # HelpInfoURI = '' 118 | 119 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 120 | # DefaultCommandPrefix = '' 121 | 122 | } 123 | 124 | -------------------------------------------------------------------------------- /AdministratorRole/AdministratorRole.psm1: -------------------------------------------------------------------------------- 1 | Set-StrictMode -Version Latest 2 | 3 | function Test-AdministratorRole { 4 | $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator") 5 | return $isAdmin 6 | 7 | <# 8 | .SYNOPSIS 9 | Gets a value indicating whether the current user is an administrator. 10 | .DESCRIPTION 11 | Returns $True or $False, depending on whether the current user is in the built-in Administatrators role 12 | .OUTPUTS 13 | System.Boolean. Returns a value indicating whether the current user is a administrator 14 | #> 15 | } 16 | 17 | function Assert-AdministratorRole { 18 | param( 19 | [parameter(position = 0)] 20 | [string]$errorMessage = "You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!" 21 | ); 22 | 23 | $isAdmin = (Test-AdministratorRole) 24 | if (-not $isAdmin) { 25 | throw $errorMessage 26 | } 27 | 28 | <# 29 | .SYNOPSIS 30 | Asserts that the current user is an administrator. Throws an error if not. 31 | .DESCRIPTION 32 | Checks whether the current user is in the built-in Administrator role, and throws an error message if not. 33 | .PARAMETER errorMessage 34 | The message to throw when the user is not an administrator 35 | #> 36 | } 37 | 38 | function Invoke-Elevated { 39 | param( 40 | [parameter(position = 0, mandatory)] 41 | [ScriptBlock] $ScriptBlock 42 | ) 43 | 44 | Start-Process Powershell -Verb runAs $ScriptBlock 45 | <# 46 | .SYNOPSIS 47 | Invokes the provided scriptblock as an administrator. 48 | .DESCRIPTION 49 | Opens a new elevated powershell which runs the provided scriptblock. 50 | .PARAMETER ScriptBlock 51 | The ScriptBlock to run in the elevated shell. 52 | #> 53 | } 54 | 55 | Export-ModuleMember Test-AdministratorRole 56 | Export-ModuleMember Assert-AdministratorRole 57 | Export-ModuleMember Invoke-Elevated -------------------------------------------------------------------------------- /BindingRedirects/BindingRedirects.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'BindingRedirects' 3 | # 4 | # Generated by: bdukes 5 | # 6 | # Generated on: 10/30/2019 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'BindingRedirects.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.1.2' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '1554e4b2-1117-4409-b3ee-198cb58f4d70' 22 | 23 | # Author of this module 24 | Author = 'Brian Dukes' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Engage Software' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2022 Engage Software' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Synchronizes web.config binding redirects with the DLL files in the bin folder' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | PowerShellVersion = '5.1.0.0' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = @('Sync-BindingRedirect') 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = @() 76 | 77 | # Variables to export from this module 78 | VariablesToExport = '*' 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = @() 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | FileList = 'BindingRedirects.psm1' 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | Tags = @('PSEdition_Core', 'PSEdition_Desktop', 'Windows') 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'https://github.com/bdukes/PowerShellModules/blob/main/LICENSE' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/bdukes/PowerShellModules' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | ReleaseNotes = 'https://github.com/bdukes/PowerShellModules/blob/main/CHANGES.md' 111 | 112 | } # End of PSData hashtable 113 | 114 | } # End of PrivateData hashtable 115 | 116 | # HelpInfo URI of this module 117 | # HelpInfoURI = '' 118 | 119 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 120 | # DefaultCommandPrefix = '' 121 | 122 | } 123 | 124 | -------------------------------------------------------------------------------- /BindingRedirects/BindingRedirects.psm1: -------------------------------------------------------------------------------- 1 | Set-StrictMode -Version Latest 2 | 3 | function Sync-BindingRedirect { 4 | param( 5 | [parameter(position = 0)] 6 | [string]$webConfigPath 7 | ); 8 | 9 | if (-not (Test-Path $webConfigPath -PathType Leaf)) { 10 | if ($webConfigPath -eq '') { 11 | $webConfigPath = 'web.config'; 12 | } 13 | else { 14 | $webConfigPath = Join-Path $webConfigPath 'web.config'; 15 | } 16 | } 17 | 18 | if (-not (Test-Path $webConfigPath -PathType Leaf)) { 19 | throw '$webConfigPath did not point to a web.config file'; 20 | } 21 | 22 | $webConfigPath = (Get-Item $webConfigPath).FullName; 23 | $websitePath = Split-Path $webConfigPath; 24 | $binPath = Join-Path $websitePath 'bin'; 25 | 26 | [xml]$config = Get-Content $webConfigPath; 27 | 28 | $assemblies = @($config.configuration.runtime.assemblyBinding.GetElementsByTagName("dependentAssembly") | Where-Object { 29 | if (Get-Member -Name 'bindingRedirect' -InputObject $_) { 30 | $assemblyFileName = "$($_.assemblyIdentity.name).dll"; 31 | $path = Join-Path $binPath $assemblyFileName; 32 | return (test-path $path) -and ([System.Reflection.AssemblyName]::GetAssemblyName($path).Version.ToString() -ne $_.bindingRedirect.newVersion); 33 | } 34 | else { return $false; } 35 | }); 36 | 37 | foreach ($assembly in $assemblies) { 38 | $assemblyFileName = "$($assembly.assemblyIdentity.name).dll"; 39 | $path = Join-Path $binPath $assemblyFileName; 40 | $assembly.bindingRedirect.newVersion = [System.Reflection.AssemblyName]::GetAssemblyName($path).Version.ToString(); 41 | } 42 | 43 | if ($assemblies.Length -gt 0) { 44 | $config.Save($webConfigPath); 45 | Write-Output "Updated $($assemblies.Length) assemblies" 46 | } 47 | else { 48 | Write-Warning 'No mismatched assemblies found' 49 | } 50 | 51 | <# 52 | .SYNOPSIS 53 | Updates the binding redirects in the web.config to match the assemblies in the bin folder 54 | .DESCRIPTION 55 | For every dependentAssembly element in the web.config, finds the matching assembly in the bin folder, and updates the newVersion attribute to match the version of the assembly file 56 | .PARAMETER webConfigPath 57 | The path to the website's web.config file 58 | #> 59 | } 60 | 61 | Export-ModuleMember Sync-BindingRedirect 62 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | - March 2025 4 | - DnnWebsiteManagement 2.0.12 5 | - Revert release of 2.0.11 6 | - DnnWebsiteManagement 2.0.11 7 | - Fix error when checking for FattMerchant and Stax columns 8 | - DnnWebsiteManagement 2.0.10 9 | - Fix error during Update Portal Alias step 10 | - February 2025 11 | - DnnWebsiteManagement 2.0.9 12 | - Fix inclusion of broken database connection logic 13 | - DnnWebsiteManagement 2.0.8 14 | - Fix incorrect original portal alias when renaming aliases 15 | - January 2025 16 | - DnnWebsiteManagement 2.0.7 17 | - Fix error connecting to SQL Server 18 | - DnnWebsiteManagement 2.0.6 19 | - Fix error when path has trailing slash, thanks ([#30](https://github.com/bdukes/PowerShellModules/pull/30), thanks [@DanielBolef](https://github.com/DanielBolef)!) 20 | - October 2024 21 | - DnnWebsiteManagement 2.0.5 22 | - Ensure DNN site's application pool is using .NET Framework 23 | - September 2024 24 | - Recycle module was delisted because of major design flaws (see [related issue](https://github.com/bdukes/PowerShellModules/issues/29)) 25 | - October 2023 26 | - DnnWebsiteManagement 2.0.4 27 | - Fix error when extracting a site with a nested folder in the zip, the site gets deleted after extracting 28 | - August 2023 29 | - DnnWebsiteManagement 2.0.3 30 | - Fix error when site has no custom aliases to rename 31 | - Fix error when web.config has a `` element around `` 32 | - Fix prompt about renaming aliases when there are no aliases to rename 33 | - July 2023 34 | - DnnWebsiteManagement 2.0.2 35 | - Fix path issues when copying site from directory instead of zip 36 | - DnnWebsiteManagement 2.0.1 37 | - Fix extraneous output from `Remove-DNNSite` 38 | - Allow passing path to `Remove-DNNSite` 39 | - Remove unnecessary file copy for typical `New-DNNSite` and `Restore-DNNSite` usage 40 | - June 2023 41 | - SslWebBinding 1.4.0 42 | - Remove certificate when removing IIS binding. 43 | - DnnWebsiteManagement 2.0.0 44 | - Remove ability to automagically look up a DNN install package on disk. Removes `-Version`, `-IncludeSource` and `-Product` parameters. 45 | - Remove host headers and certificates when removing a site. 46 | - DnnWebsiteManagement 1.8.0 47 | - Allow interactively choosing new portal aliases during restore ([#24](https://github.com/bdukes/PowerShellModules/pull/24), thanks [@engage-chancock](https://github.com/engage-chancock)!) 48 | - Allow passing a zipped backup 49 | - Fix issue with script requesting database name 50 | - April 2023 51 | - DnnWebsiteManagement 1.7.1 52 | - Don't show warning about obsolete encrypt parameter 53 | - DnnWebsiteManagement 1.7.0 54 | - Don't encrypt connections to SQL Server 55 | - December 2022 56 | - DnnWebsiteManagement 1.6.2 57 | - Fix issue creating a site from a version instead of a zip 58 | - October 2022 59 | - BindingRedirects 0.1.2 60 | - Fix issue `web.config` contains `dependentAssembly` without `bindingRedirect` element (e.g. when `codeBase` element is used instead) 61 | - September 2022 62 | - DnnWebsiteManagement 1.6.1 63 | - Fix issue when `GitRepository` is passed on Windows Powershell 64 | - Show progress when copying Git repository 65 | - Move files from site backup instead of copy and delete (when backup is a zip, not a folder) 66 | - DnnWebsiteManagement 1.6.0 67 | - Add `GitRepository` parameter to `New-DNNSite` and `Restore-DNNSite` 68 | - If `GitRepository` or `SiteZipPath` includes `.dnn-website-management/restore-site.ps1`, this script is called at the end of `Restore-DNNSite` 69 | - August 2022 70 | - SslWebBinding 1.3.0 71 | - `New-SslWebBinding` will use `mkcert` to generate the certificate if installed 72 | - `Remove-SslWebBinding` correctly suppresses confirm prompt 73 | - DnnWebsiteManagement 1.4.2 74 | - Use SslWebBinding 1.3.0 75 | - DnnWebsiteManagement 1.4.3 76 | - Fix error when site zip has a single folder 77 | - DnnWebsiteManagement 1.4.4 78 | - Fix error when restoring newer Engage: AMS site 79 | - DnnWebsiteManagement 1.4.5 80 | - Remove extra confirmation prompts 81 | - DnnWebsiteManagement 1.5.0 82 | - Fail fast when unable to continue 83 | - Use standardized names (with aliases for backwards compatibility) 84 | - Rename `Upgrade-DNNSite` to `Update-DNNSite` 85 | - Rename `Install-DNNResources` to `Install-DNNResource` 86 | - Capitalize all parameters 87 | - Rename `siteName` to `Name` 88 | - Rename `siteZip` to `SiteZipPath` 89 | - Rename `oldDomain` to `Domain` 90 | - etc. 91 | - Implement ShouldProcess (i.e. `-WhatIf` and `-Confirm`) for `Rename-DNNSite` and `Update-DNNSite` 92 | - Support restoring when site zip includes development files (i.e. if the website folder is a level deeper but the top-level files should be kept) 93 | - Show progress when copying files and restoring database 94 | - DnnWebsiteManagement 1.5.1 95 | - Fix error when no database backup is passed 96 | - July 2022 97 | - Recycle 1.5.0 98 | - Added `Restore-RecycledItem` and `Get-RecycledItem` 99 | - AdministratorRole 1.1.0 100 | - Added `Invoke-Elevated` 101 | - April 2022 102 | - DnnWebsiteManagement 1.4.1 103 | - Fix errors introduced in 1.4.0 for `Restore-DNNSite` 104 | - DnnWebsiteManagement 1.4.0 105 | - Add more protection scripts when restoring 106 | - Update default DNN version to 9.10.2 107 | - Implement ShouldProcess (i.e. `-WhatIf` and `-Confirm`) for `New-DNNSite` and `Remove-DNNSite` 108 | - Use SqlServer module instead of SQLPS 109 | - Use IisAdministration module instead of WebAdministration 110 | - ACL-Permissions 1.1.0 111 | - Implement ShouldProcess (i.e. `-WhatIf` and `-Confirm`) for `Set-ModifyPermission` 112 | - Use IisAdministration module instead of WebAdministration 113 | - SslWebBinding 1.2.0 114 | - Implement ShouldProcess (i.e. `-WhatIf` and `-Confirm`) for `New-SslWebBinding` and `Remove-SslWebBinding` 115 | - Use IisAdministration module instead of WebAdministration 116 | - Add-HostFileEntry 1.1.0 117 | - Implement ShouldProcess (i.e. `-WhatIf` and `-Confirm`) for `Add-HostFileEntry` and `Remove-HostFileEntry` 118 | - September 2021 119 | - Declare platform compatibility 120 | - ACL-Permissions 1.0.2 121 | - Add-HostFileEntry 1.0.4 122 | - AdministratorRole 1.0.1 123 | - BindingRedirects 0.1.1 124 | - DnnWebsiteManagement 1.3.1 125 | - Read-Choice 1.0.1 126 | - Recycle 1.3.1 127 | - SslWebBinding 1.1.2 128 | - Write-HtmlNode 2.0.1 129 | - May 2021 130 | - Recycle 1.3.0 131 | - Add support for piping files into Remove-ItemSafely 132 | - March 2021 133 | - DnnWebsiteManagement 1.3.0 134 | - Remove Application Insights config when restoring 135 | - Recycle 1.2.0 136 | - Add ShouldProcess (i.e. -WhatIf and -Confirm) support 137 | - Write-HtmlNode 2.0.0 138 | - Indicate it only supports Desktop edition (i.e. Windows Powershell vs. Powershell Core) 139 | - October 2019 140 | - BindingRedirects 0.1.0 141 | - Initial version 142 | - June 2019 143 | - DnnWebsiteManagement 1.2.4 144 | - Fix bugs extracting package and viewing zip error output 145 | - DnnWebsiteManagement 1.2.3 146 | - Fix failure to clean up extracted files after restore 147 | - DnnWebsiteManagement 1.2.2 148 | - Removed (hidden) dependency on PSCX 149 | - February 2019 150 | - Add-HostFileEntry 1.0.2 151 | - Removed (hidden) dependency on PSCX 152 | - DnnWebsiteManagement 1.2.1 153 | - Removed dependency on PSCX 154 | - August 2018 155 | - Recycle 1.1.1 156 | - Removed wildcard exports for increased performance and security 157 | - July 2018 158 | - SslWebBinding 1.1.1 159 | - Removed duplicate host headers when generating binding and certificate 160 | - Mar. 2018 161 | - Recycle 1.1.0 162 | - Added ability to remove files with special characters in path via `-LiteralPath` -parameter 163 | - Added ability to remove multiple files by passing a glob, e.g. `Remove-ItemSafely -Path *.txt` 164 | - Nov. 2017 165 | - Added ability to generate HTTPS certificate with multiple domains in `SslWebBinding` 166 | - When restoring DNN site in `DnnWebsiteManagement`, generate single HTTPS certificate 167 | - Oct. 2016 168 | - Added `Read-Choice` module 169 | - Added `Write-HtmlNode` module 170 | - Added `SslWebBinding` module 171 | - Added `AdministratorRole` module 172 | - Added `Add-HostFileEntry` module 173 | - Added `ACL-Permissions` module 174 | - Added `DnnWebsiteManagement` module 175 | - Aug. 2016 176 | - Added `Recycle` module 177 | -------------------------------------------------------------------------------- /DnnWebsiteManagement/DnnWebsiteManagement.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'DnnWebsiteManagement' 3 | # 4 | # Generated by: Brian Dukes 5 | # 6 | # Generated on: 10/10/2016 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'DnnWebsiteManagement.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '2.0.12' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '53547b4e-358b-49ad-9e8b-7ea0ef271524' 22 | 23 | # Author of this module 24 | Author = 'Brian Dukes' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Engage Software' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2025 Engage Software' 31 | 32 | # Description of the functionality provided by this module 33 | Description = "A set of functions for managing websites built on the DNN Platform." 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | PowerShellVersion = '5.0.0' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | RequiredModules = @( @{ ModuleName = 'AdministratorRole'; ModuleVersion = '1.1.0'; GUID = '694c2097-6b13-4735-8d6e-396224d646cc' }, 55 | @{ ModuleName = 'Add-HostFileEntry'; ModuleVersion = '1.1.0'; GUID = '16e30c8c-8de5-4090-a542-e8f9594ca613' }, 56 | @{ ModuleName = 'SslWebBinding'; ModuleVersion = '1.4.0'; GUID = 'd8b5b233-6f01-4ade-b771-147cc9101072' }, 57 | @{ ModuleName = 'Write-HtmlNode'; ModuleVersion = '2.0.1'; GUID = '941aad91-17a6-43a5-bb1c-cce8526d7b3e' }, 58 | @{ ModuleName = 'ACL-Permissions'; ModuleVersion = '1.1.0'; GUID = '908787a0-5a50-43c8-816a-7fa411b4e562' }, 59 | @{ ModuleName = 'Read-Choice'; ModuleVersion = '1.0.2'; GUID = 'ebab63fa-f63d-427a-99c8-8450974b257c' }, 60 | @{ ModuleName = 'SqlServer'; ModuleVersion = '21.1.18256' }, 61 | @{ ModuleName = 'IISAdministration'; ModuleVersion = '1.1.0.0' } ) 62 | 63 | # Assemblies that must be loaded prior to importing this module 64 | # RequiredAssemblies = @() 65 | 66 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 67 | # ScriptsToProcess = @() 68 | 69 | # Type files (.ps1xml) to be loaded when importing this module 70 | # TypesToProcess = @() 71 | 72 | # Format files (.ps1xml) to be loaded when importing this module 73 | # FormatsToProcess = @() 74 | 75 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 76 | # NestedModules = @() 77 | 78 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 79 | FunctionsToExport = @('Install-DNNResource', 'Remove-DNNSite', 'Rename-DNNSite', 'New-DNNSite', 'Update-DNNSite', 'Restore-DNNSite') 80 | 81 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 82 | CmdletsToExport = @() 83 | 84 | # Variables to export from this module 85 | VariablesToExport = @() 86 | 87 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 88 | AliasesToExport = @() 89 | 90 | # DSC resources to export from this module 91 | # DscResourcesToExport = @() 92 | 93 | # List of all modules packaged with this module 94 | # ModuleList = @() 95 | 96 | # List of all files packaged with this module 97 | FileList = 'DnnWebsiteManagement.psm1' 98 | 99 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 100 | PrivateData = @{ 101 | 102 | PSData = @{ 103 | 104 | # Tags applied to this module. These help with module discovery in online galleries. 105 | Tags = @('PSEdition_Core', 'PSEdition_Desktop', 'Windows') 106 | 107 | # A URL to the license for this module. 108 | LicenseUri = 'https://github.com/bdukes/PowerShellModules/blob/main/LICENSE' 109 | 110 | # A URL to the main website for this project. 111 | ProjectUri = 'https://github.com/bdukes/PowerShellModules' 112 | 113 | # A URL to an icon representing this module. 114 | # IconUri = '' 115 | 116 | # ReleaseNotes of this module 117 | ReleaseNotes = 'https://github.com/bdukes/PowerShellModules/blob/main/CHANGES.md' 118 | 119 | ExternalModuleDependencies = @() 120 | 121 | } # End of PSData hashtable 122 | 123 | } # End of PrivateData hashtable 124 | 125 | # HelpInfo URI of this module 126 | # HelpInfoURI = '' 127 | 128 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 129 | # DefaultCommandPrefix = '' 130 | 131 | } 132 | 133 | -------------------------------------------------------------------------------- /DnnWebsiteManagement/DnnWebsiteManagement.psm1: -------------------------------------------------------------------------------- 1 | #Requires -Version 3 2 | #Requires -Modules Add-HostFileEntry, AdministratorRole, PKI, SslWebBinding, SqlServer, IISAdministration, Read-Choice 3 | Set-StrictMode -Version:Latest 4 | 5 | $www = $env:www 6 | if ($null -eq $www) { 7 | $inetpub = Join-Path 'C:' -ChildPath:'inetpub'; 8 | $www = Join-Path $inetpub 'wwwroot'; 9 | } 10 | 11 | function Install-DNNResource { 12 | [Alias("Install-DNNResources")] 13 | param( 14 | [Alias("siteName")] 15 | [parameter(Mandatory = $false, position = 0)] 16 | [string]$Name 17 | ); 18 | 19 | if ($Name -eq '' -and $PWD.Provider.Name -eq 'FileSystem' -and $PWD.Path.StartsWith($www)) { 20 | $pathParts = $PWD.Path -split '[\\/]'; 21 | $pathIndex = ($www -split '[\\/]').Length; 22 | $Name = $pathParts[$pathIndex]; 23 | Write-Verbose "Site name is '$Name'"; 24 | } 25 | 26 | if ($Name -eq '') { 27 | throw 'You must specify the site name (e.g. dnn.local) if you are not in the website' 28 | } 29 | 30 | try { 31 | $result = Invoke-WebRequest "https://$Name/Install/Install.aspx?mode=InstallResources" 32 | 33 | if ($result.StatusCode -ne 200) { 34 | 35 | Write-Warning "There was an error trying to install the resources: Status code $($result.StatusCode)" 36 | return 37 | } 38 | 39 | Write-HtmlNode $result.ParsedHtml.documentElement -excludeAttributes -excludeEmptyElements -excludeComments 40 | } 41 | catch { 42 | Write-Warning "There was an error trying to install the resources: $_" 43 | } 44 | 45 | <# 46 | .SYNOPSIS 47 | Kicks off any pending extension package installations 48 | .DESCRIPTION 49 | Starts the Install Resources mode of the installer, installing all extension packages in the Install folder of the website 50 | .PARAMETER Name 51 | The name of the site (the domain, folder name, and database name, e.g. dnn.local). If not specified, this is derived from the current folder path 52 | #> 53 | } 54 | 55 | function Remove-DNNSite { 56 | [CmdletBinding(SupportsShouldProcess)] 57 | param( 58 | [Alias("siteName")] 59 | [parameter(Mandatory = $true, position = 0)] 60 | [string]$Name 61 | ); 62 | 63 | Assert-AdministratorRole 64 | 65 | # Allow passing in a path, rather than a name 66 | $sitePath = Join-Path $www $Name; 67 | $Name = Split-Path -Path $sitePath -Leaf; 68 | 69 | $hostHeaders = [System.Collections.Generic.HashSet[string]]@($Name); 70 | 71 | $website = Get-IISSite $Name; 72 | if ($website) { 73 | foreach ($binding in $website.Bindings) { 74 | $hostHeader = $binding.bindingInformation.Substring(6) #remove "*:443:" from the beginning of the binding info 75 | $hostHeaders.Add($hostHeader) | Out-Null; 76 | } 77 | 78 | foreach ($hostHeader in $hostHeaders) { 79 | if ($PSCmdlet.ShouldProcess($hostHeader, 'Remove HTTPS Binding')) { 80 | Remove-SslWebBinding $Name $hostHeader -Confirm:$false -ErrorAction:SilentlyContinue; 81 | } 82 | } 83 | 84 | if ($PSCmdlet.ShouldProcess($Name, 'Remove IIS Site')) { 85 | Remove-IISSite $Name -WhatIf:$WhatIfPreference -Confirm:$false -ErrorAction:SilentlyContinue; 86 | } 87 | } 88 | 89 | $serverManager = Get-IISServerManager; 90 | $appPool = $serverManager.ApplicationPools[$Name]; 91 | if ($appPool) { 92 | Write-Information "Removing $Name app pool from IIS" 93 | 94 | if ($PSCmdlet.ShouldProcess($Name, 'Remove IIS App Pool')) { 95 | $appPool.Delete(); 96 | $serverManager.CommitChanges(); 97 | } 98 | } 99 | else { 100 | Write-Information "$Name app pool not found in IIS" 101 | } 102 | 103 | if (Test-Path $sitePath) { 104 | if ($PSCmdlet.ShouldProcess($sitePath, "Remove website folder")) { 105 | Remove-Item $sitePath -Recurse -Force -WhatIf:$WhatIfPreference -Confirm:$false; 106 | } 107 | } 108 | else { 109 | Write-Information "$sitePath does not exist" 110 | } 111 | 112 | $sqlPath = Join-Path 'SQLSERVER:' 'SQL'; 113 | $localhostSqlPath = Join-Path $sqlPath '(local)'; 114 | $localSqlPath = Join-Path $localhostSqlPath 'DEFAULT'; 115 | $databasesSqlPath = Join-Path $localSqlPath 'Databases'; 116 | $databasePath = Join-Path $databasesSqlPath (ConvertTo-EncodedSqlName $Name); 117 | if (Test-Path $databasePath) { 118 | if ($PSCmdlet.ShouldProcess($Name, 'Drop Database')) { 119 | invokeSql -Query:"ALTER DATABASE [$Name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;" -Database:master 120 | invokeSql -Query:"DROP DATABASE [$Name];" -Database:master 121 | } 122 | } 123 | else { 124 | Write-Information "$Name database not found" 125 | } 126 | 127 | $loginName = "IIS AppPool\$Name"; 128 | $loginsPath = Join-Path $localSqlPath 'Logins'; 129 | $loginPath = Join-Path $loginsPath (ConvertTo-EncodedSqlName $loginName); 130 | if (Test-Path $loginPath) { 131 | if ($PSCmdlet.ShouldProcess($loginName, 'Drop login')) { 132 | invokeSql -Query:"DROP LOGIN [$loginName];" -Database:master 133 | } 134 | } 135 | else { 136 | Write-Information "$loginName database login not found" 137 | } 138 | 139 | foreach ($hostHeader in $hostHeaders) { 140 | if ($PSCmdlet.ShouldProcess($hostHeader, 'Remove HOSTS file entry')) { 141 | Remove-HostFileEntry $hostHeader -WhatIf:$WhatIfPreference -Confirm:$false -ErrorAction:SilentlyContinue; 142 | } 143 | } 144 | 145 | <# 146 | .SYNOPSIS 147 | Destroys a DNN site 148 | .DESCRIPTION 149 | Destroys a DNN site, removing it from the file system, IIS, and the database 150 | .PARAMETER Name 151 | The name of the site (the domain, folder name, and database name, e.g. dnn.local) 152 | #> 153 | } 154 | 155 | function Rename-DNNSite { 156 | [CmdletBinding(SupportsShouldProcess)] 157 | param( 158 | [Alias("oldSiteName")] 159 | [Alias("oldName")] 160 | [parameter(Mandatory = $true, position = 0)] 161 | [string]$Name, 162 | 163 | [Alias("newSiteName")] 164 | [parameter(Mandatory = $true, position = 1)] 165 | [string]$NewName 166 | ); 167 | 168 | Assert-AdministratorRole 169 | 170 | $serverManager = Get-IISServerManager 171 | $appPool = $serverManager.ApplicationPools[$Name] 172 | if ($appPool -and $appPool.State -eq 'Started') { 173 | if ($PSCmdlet.ShouldProcess("$Name", "Stop IIS app pool")) { 174 | $appPool.Stop() 175 | while ($appPool.State -ne 'Stopped') { 176 | Start-Sleep -m 100 177 | } 178 | } 179 | } 180 | 181 | $oldSitePath = Join-Path $www $Name; 182 | $newSitePath = Join-Path $www $NewName; 183 | if (Test-Path $oldSitePath) { 184 | if ($PSCmdlet.ShouldProcess($oldSitePath, "Rename to $newSitePath")) { 185 | Rename-Item $oldSitePath $newSitePath -WhatIf:$WhatIfPreference -Confirm:$false; 186 | } 187 | } 188 | else { 189 | Write-Information "$oldSitePath does not exist" 190 | } 191 | 192 | $newWebsitePath = Join-Path $newSitePath 'Website'; 193 | $website = $serverManager.Sites[$Name]; 194 | $app = $website.Applications['/']; 195 | $virtualDirectory = $app.VirtualDirectories['/']; 196 | $virtualDirectory.PhysicalPath = $newWebsitePath; 197 | if ($PSCmdlet.ShouldProcess("*:80:$Name", "Rename IIS site binding to *:80:$NewName")) { 198 | Remove-IISSiteBinding -Name:$Name -BindingInformation:"*:80:$Name" 199 | New-IISSiteBinding -Name:$Name -BindingInformation:"*:80:$NewName" -Protocol:'http' 200 | } 201 | 202 | if ($PSCmdlet.ShouldProcess("$Name", "Rename IIS site to $NewName")) { 203 | $website.Name = $NewName; 204 | } 205 | if ($PSCmdlet.ShouldProcess("$Name", "Rename IIS app pool to $NewName")) { 206 | $appPool.Name = $NewName; 207 | $app.ApplicationPoolName = $NewName; 208 | } 209 | 210 | $serverManager.CommitChanges(); 211 | 212 | $sqlPath = Join-Path 'SQLSERVER:' 'SQL'; 213 | $localhostSqlPath = Join-Path $sqlPath '(local)'; 214 | $localSqlPath = Join-Path $localhostSqlPath 'DEFAULT'; 215 | $databasesSqlPath = Join-Path $localSqlPath 'Databases'; 216 | $databasePath = Join-Path $databasesSqlPath (ConvertTo-EncodedSqlName $Name); 217 | if (Test-Path $databasePath) { 218 | if ($PSCmdlet.ShouldProcess("$Name", "Close database connection")) { 219 | invokeSql -Query:"ALTER DATABASE [$Name] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;" -Database:master 220 | } 221 | if ($PSCmdlet.ShouldProcess("$Name", "Rename database to $NewName")) { 222 | invokeSql -Query:"ALTER DATABASE [$Name] MODIFY NAME = [$NewName];" -Database:master 223 | invokeSql -Query:"ALTER DATABASE [$NewName] SET MULTI_USER WITH ROLLBACK IMMEDIATE;" -Database:master 224 | } 225 | } 226 | else { 227 | Write-Information "$Name database not found" 228 | } 229 | 230 | $oldLoginName = "IIS AppPool\$Name"; 231 | $newLoginName = "IIS AppPool\$NewName"; 232 | $loginsPath = Join-Path $localSqlPath 'Logins'; 233 | $newLoginPath = Join-Path $loginsPath (ConvertTo-EncodedSqlName $newLoginName); 234 | if (-not (Test-Path $newLoginPath)) { 235 | if ($PSCmdlet.ShouldProcess($newLoginName, "Create SQL Server login")) { 236 | invokeSql -Query:"CREATE LOGIN [$newLoginName] FROM WINDOWS WITH DEFAULT_DATABASE = [$NewName];" -Database:master 237 | } 238 | } 239 | 240 | if ($PSCmdlet.ShouldProcess($newLoginName, "Create SQL Server user")) { 241 | invokeSql -Query:"CREATE USER [$newLoginName] FOR LOGIN [$newLoginName];" -Database:$NewName 242 | } 243 | 244 | if ($PSCmdlet.ShouldProcess($newLoginName, "Add SQL Server user to db_owner role")) { 245 | invokeSql -Query:"EXEC sp_addrolemember N'db_owner', N'$newLoginName';" -Database:$NewName 246 | } 247 | 248 | $ownedRoles = invokeSql -Query:"SELECT p2.name FROM sys.database_principals p1 JOIN sys.database_principals p2 ON p1.principal_id = p2.owning_principal_id WHERE p1.name = '$newLoginName';" -Database:$NewName 249 | foreach ($roleRow in $ownedRoles) { 250 | $roleName = $roleRow.name 251 | if ($PSCmdlet.ShouldProcess("$roleName", "Transfer role ownership to $newLoginName")) { 252 | invokeSql -Query:"ALTER AUTHORIZATION ON ROLE::[$roleName] TO [$newLoginName];" -Database:$NewName 253 | } 254 | } 255 | 256 | if ($PSCmdlet.ShouldProcess($oldLoginName, "Drop SQL Server user")) { 257 | invokeSql -Query:"DROP USER [$oldLoginName];" -Database:$NewName 258 | } 259 | 260 | $oldLoginPath = Join-Path $loginsPath (ConvertTo-EncodedSqlName $oldLoginName); 261 | if (Test-Path $oldLoginPath) { 262 | if ($PSCmdlet.ShouldProcess($oldLoginName, "Drop SQL Server login")) { 263 | invokeSql -Query:"DROP LOGIN [$oldLoginName];" -Database:master 264 | } 265 | } 266 | else { 267 | Write-Information "$oldLoginName database login not found" 268 | } 269 | 270 | if ($PSCmdlet.ShouldProcess($newWebsitePath, "Set Modify File Permissions")) { 271 | Set-ModifyPermission -Directory:$newWebsitePath -Username:$NewName -WhatIf:$WhatIfPreference -Confirm:$false 272 | } 273 | 274 | $webConfigPath = Join-Path $newWebsitePath 'web.config'; 275 | if ($PSCmdlet.ShouldProcess($webConfigPath, "Update connection string")) { 276 | [xml]$webConfig = Get-Content $webConfigPath 277 | $ObjectQualifier = $webConfig.configuration.dotnetnuke.data.providers.add.objectQualifier.TrimEnd('_') 278 | $DatabaseOwner = $webConfig.configuration.dotnetnuke.data.providers.add.databaseOwner.TrimEnd('.') 279 | $connectionString = "Data Source=.`;Initial Catalog=$NewName`;Integrated Security=true" 280 | $webConfig.configuration.connectionStrings.add | Where-Object { $_.name -eq 'SiteSqlServer' } | ForEach-Object { $_.connectionString = $connectionString } 281 | $webConfig.configuration.appSettings.add | Where-Object { $_.key -eq 'SiteSqlServer' } | ForEach-Object { $_.value = $connectionString } 282 | $webConfig.Save($webConfigPath) 283 | } 284 | 285 | if ($PSCmdlet.ShouldProcess("$NewName", "Replace $Name in portal aliases")) { 286 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'PortalAlias' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET HTTPAlias = REPLACE(HTTPAlias, '$Name', '$NewName')" -Database:$NewName 287 | } 288 | 289 | if ($PSCmdlet.ShouldProcess("$Name", "Remove HOSTS file entry")) { 290 | Remove-HostFileEntry $Name -WhatIf:$WhatIfPreference -Confirm:$false 291 | } 292 | if ($PSCmdlet.ShouldProcess("$NewName", "Add HOSTS file entry")) { 293 | Add-HostFileEntry $NewName -WhatIf:$WhatIfPreference -Confirm:$false 294 | } 295 | 296 | $appPool.Start(); 297 | 298 | if ($PSCmdlet.ShouldProcess("https://$NewName", "Open browser")) { 299 | if (Get-Command -Name:Start-Process -ParameterName:WhatIf -ErrorAction SilentlyContinue) { 300 | Start-Process -FilePath:https://$NewName -WhatIf:$WhatIfPreference -Confirm:$false; 301 | } 302 | else { 303 | Start-Process -FilePath:https://$NewName; 304 | } 305 | } 306 | 307 | <# 308 | .SYNOPSIS 309 | Renames a DNN site 310 | .DESCRIPTION 311 | Renames a DNN site in the file system, IIS, and the database 312 | .PARAMETER Name 313 | The current name of the site (the domain, folder name, and database name, e.g. dnn.local) 314 | .PARAMETER NewName 315 | The new name to which the site should be renamed 316 | #> 317 | } 318 | 319 | function Restore-DNNSite { 320 | [CmdletBinding(SupportsShouldProcess)] 321 | param( 322 | [Alias("siteName")] 323 | [parameter(Mandatory = $true, position = 0)] 324 | [string]$Name, 325 | 326 | [Alias("siteZip")] 327 | [parameter(Mandatory = $true, position = 1)] 328 | [ValidateScript({ if (Test-Path -Path:$_) { $true; } else { throw "$_ file or directory not found" } })] 329 | [string]$SiteZipPath, 330 | 331 | [Alias("databaseBackup")] 332 | [parameter(Mandatory = $true, position = 2)] 333 | [ValidateScript({ if (Test-Path -Path:$_ -PathType:Leaf) { $true; } else { throw "$_ file not found" } })] 334 | [string]$DatabaseBackupPath, 335 | 336 | [Alias("oldDomain")] 337 | [parameter(Mandatory = $false)] 338 | [string]$Domain = '', 339 | 340 | [parameter(Mandatory = $false)] 341 | [string]$GitRepository = '', 342 | 343 | [parameter(Mandatory = $false)] 344 | [switch]$Interactive 345 | ); 346 | 347 | $databaseBackupFolder = ''; 348 | $siteZipFile = Get-Item $SiteZipPath 349 | if ($siteZipFile.Extension -eq '.bak') { 350 | $SiteZipPath = $DatabaseBackupPath 351 | $DatabaseBackupPath = $siteZipFile.FullName 352 | } 353 | else { 354 | $DatabaseBackupFile = Get-Item $DatabaseBackupPath; 355 | if ($DatabaseBackupFile.Extension -ne '.bak') { 356 | if ($PSCmdlet.ShouldProcess($DatabaseBackupPath, 'Unzip backup file')) { 357 | $databaseBackupFolder = Join-Path -Path $([System.IO.Path]::GetTempPath()) -ChildPath "dnn-website-management-db-$(Get-Date -Format 'yyyyMMddHHmmss')"; 358 | extractZip -output:$databaseBackupFolder -zipFile:$DatabaseBackupPath; 359 | $DatabaseBackupPath = Get-Item -Path:"$databaseBackupFolder/*.bak"; 360 | } 361 | } 362 | } 363 | 364 | New-DNNSite $Name -SiteZipPath:$SiteZipPath -DatabaseBackupPath:$DatabaseBackupPath -Domain:$Domain -GitRepository:$GitRepository -Interactive:$Interactive; 365 | 366 | $sitePath = Join-Path $www $Name; 367 | $scriptsDir = Join-Path $sitePath '.dnn-website-management'; 368 | $restoreScript = Join-Path $scriptsDir 'restore-site.ps1'; 369 | if ((Test-Path $restoreScript)) { 370 | $restoreArgs = @{}; 371 | $restoreCmd = Get-Command $restoreScript; 372 | if ($restoreCmd.Parameters.ContainsKey('Name')) { 373 | $restoreArgs['Name'] = $Name; 374 | } 375 | elseif ($restoreCmd.Parameters.ContainsKey('siteName')) { 376 | $restoreArgs['siteName'] = $Name; 377 | } 378 | if ($restoreCmd.Parameters.ContainsKey('SiteZipPath')) { 379 | $restoreArgs['SiteZipPath'] = $SiteZipPath; 380 | } 381 | elseif ($restoreCmd.Parameters.ContainsKey('siteZip')) { 382 | $restoreArgs['siteZip'] = $SiteZipPath; 383 | } 384 | if ($restoreCmd.Parameters.ContainsKey('DatabaseBackupPath')) { 385 | $restoreArgs['DatabaseBackupPath'] = $DatabaseBackupPath; 386 | } 387 | elseif ($restoreCmd.Parameters.ContainsKey('databaseBackup')) { 388 | $restoreArgs['databaseBackup'] = $DatabaseBackupPath; 389 | } 390 | if ($restoreCmd.Parameters.ContainsKey('Domain')) { 391 | $restoreArgs['Domain'] = $Domain; 392 | } 393 | elseif ($restoreCmd.Parameters.ContainsKey('oldDomain')) { 394 | $restoreArgs['oldDomain'] = $Domain; 395 | } 396 | if ($restoreCmd.Parameters.ContainsKey('GitRepository')) { 397 | $restoreArgs['GitRepository'] = $GitRepository; 398 | } 399 | if ($restoreCmd.Parameters.ContainsKey('WhatIf')) { 400 | $restoreArgs['WhatIf'] = $WhatIfPreference; 401 | } 402 | if ($restoreCmd.Parameters.ContainsKey('Confirm')) { 403 | $restoreArgs['Confirm'] = $ConfirmPreference -eq 'Low'; 404 | } 405 | if ($restoreCmd.Parameters.ContainsKey('Verbose')) { 406 | $restoreArgs['Verbose'] = $VerbosePreference -eq 'Continue'; 407 | } 408 | 409 | if ($PSCmdlet.ShouldProcess($restoreScript, 'Run restore script') -or $restoreArgs['WhatIf']) { 410 | & $restoreScript @restoreArgs; 411 | } 412 | } 413 | 414 | if ($databaseBackupFolder -and $PSCmdlet.ShouldProcess($databaseBackupFolder, 'Delete database backup folder')) { 415 | Remove-Item $databaseBackupFolder -Force -Recurse; 416 | } 417 | 418 | <# 419 | .SYNOPSIS 420 | Restores a backup of a DNN site 421 | .DESCRIPTION 422 | Restores a DNN site from a file system zip and database backup 423 | .PARAMETER Name 424 | The name of the site (the domain, folder name, and database name, e.g. dnn.local) 425 | .PARAMETER SiteZipPath 426 | The full path to the zip (any format that 7-Zip can expand) of the site's file system, or the full path to a folder with the site's contents 427 | .PARAMETER DatabaseBackupPath 428 | The full path to the database backup (.bak file). This must be in a location to which SQL Server has access 429 | .PARAMETER Domain 430 | If specified, the Portal Alias table will be updated to replace the old site domain with the new site domain 431 | .PARAMETER GitRepository 432 | If specified, the git repository at the given URL/path will be cloned into the site's folder 433 | .PARAMETER Interactive 434 | Whether the cmdlet can prompt the user for additional information (e.g. how to rename additional portal aliases) 435 | #> 436 | } 437 | 438 | function Update-DNNSite { 439 | [Alias("Upgrade-DNNSite")] 440 | [CmdletBinding(SupportsShouldProcess)] 441 | param( 442 | [Alias("siteName")] 443 | [parameter(Mandatory = $true, position = 0)] 444 | [string]$Name, 445 | 446 | [parameter(Mandatory = $true, position = 1)] 447 | [string]$SiteZipPath 448 | ); 449 | try { 450 | if ($PSCmdlet.ShouldProcess($SiteZipPath, "Extract upgrade package")) { 451 | extractPackages -Name:$Name -SiteZipPath:$SiteZipPath -ErrorAction Stop; 452 | } 453 | } 454 | catch { 455 | $PSCmdlet.ThrowTerminatingError($_); 456 | } 457 | 458 | if ($PSCmdlet.ShouldProcess("https://$Name/Install/Install.aspx?mode=upgrade", "Open browser")) { 459 | if (Get-Command -Name:Start-Process -ParameterName:WhatIf -ErrorAction SilentlyContinue) { 460 | Start-Process -FilePath:https://$Name/Install/Install.aspx?mode=upgrade -WhatIf:$WhatIfPreference -Confirm:$false; 461 | } 462 | else { 463 | Start-Process -FilePath:https://$Name/Install/Install.aspx?mode=upgrade; 464 | } 465 | } 466 | 467 | <# 468 | .SYNOPSIS 469 | Upgrades a DNN site 470 | .DESCRIPTION 471 | Upgrades an existing DNN site using the specified upgrade package 472 | .PARAMETER Name 473 | The name of the site (the domain, folder name, and database name, e.g. dnn.local) 474 | .PARAMETER SiteZipPath 475 | The path to the upgrade package zip. 476 | #> 477 | } 478 | 479 | function New-DNNSite { 480 | [CmdletBinding(SupportsShouldProcess)] 481 | param( 482 | [Alias("siteName")] 483 | [parameter(Mandatory = $true, position = 0)] 484 | [string]$Name, 485 | 486 | [Alias("siteZip")] 487 | [parameter(Mandatory = $true, position = 1)] 488 | [string]$SiteZipPath, 489 | 490 | [string]$ObjectQualifier = '', 491 | 492 | [string]$DatabaseOwner = 'dbo', 493 | 494 | [Alias("databaseBackup")] 495 | [string]$DatabaseBackupPath = '', 496 | 497 | [Alias("oldDomain")] 498 | [string]$Domain = '', 499 | 500 | [string]$GitRepository = '', 501 | 502 | [switch]$Interactive 503 | ); 504 | 505 | Assert-AdministratorRole 506 | 507 | $NameExtension = [System.IO.Path]::GetExtension($Name) 508 | if ($NameExtension -eq '') { $NameExtension = '.local' } 509 | 510 | if ($PSCmdlet.ShouldProcess($Name, 'Extract Package')) { 511 | try { 512 | extractPackages -Name:$Name -SiteZip:$SiteZipPath -ErrorAction Stop; 513 | } 514 | catch { 515 | $PSCmdlet.ThrowTerminatingError($_); 516 | } 517 | } 518 | 519 | if ($PSCmdlet.ShouldProcess($Name, 'Add HOSTS file entry')) { 520 | Add-HostFileEntry $Name 521 | } 522 | 523 | $serverManager = Get-IISServerManager; 524 | if ($PSCmdlet.ShouldProcess($Name, 'Create IIS App Pool')) { 525 | $appPool = $serverManager.ApplicationPools.Add($Name); 526 | $appPool.ManagedPipelineMode = 'Integrated'; 527 | $appPool.ManagedRuntimeVersion = 'v4.0'; 528 | $serverManager.CommitChanges(); 529 | } 530 | 531 | $sitePath = Join-Path $www $Name; 532 | $websitePath = Join-Path $sitePath 'Website'; 533 | if ($PSCmdlet.ShouldProcess($Name, 'Create IIS Site')) { 534 | $website = $serverManager.Sites.Add($Name, 'http', "*:80:$Name", $websitePath); 535 | $website.Applications['/'].ApplicationPoolName = $Name; 536 | $serverManager.CommitChanges(); 537 | } 538 | 539 | $domains = New-Object System.Collections.Generic.List[System.String] 540 | $domains.Add($Name) 541 | 542 | if ($PSCmdlet.ShouldProcess($websitePath, 'Set Modify File Permissions')) { 543 | Set-ModifyPermission -Directory:$websitePath -Username:$Name -WhatIf:$WhatIfPreference -Confirm:$false; 544 | } 545 | 546 | if ($GitRepository -and $PSCmdlet.ShouldProcess($GitRepository, 'Git clone')) { 547 | $clonePath = Join-Path $sitePath 'Temp_GitClone'; 548 | git clone $GitRepository $clonePath; 549 | 550 | moveWithProgress -from:$clonePath -to:$sitePath; 551 | Remove-Item $clonePath -Recurse -Force -Confirm:$false; 552 | } 553 | 554 | $webConfigPath = Join-Path $websitePath 'web.config'; 555 | [xml]$webConfig = Get-Content $webConfigPath; 556 | if ($DatabaseBackupPath -eq '') { 557 | if ($PSCmdlet.ShouldProcess($Name, 'Create Database')) { 558 | newDnnDatabase $Name -ErrorAction Stop; 559 | } 560 | # TODO: create schema if $DatabaseOwner has been passed in 561 | } 562 | else { 563 | if ($PSCmdlet.ShouldProcess($DatabaseBackupPath, 'Restore Database')) { 564 | restoreDnnDatabase $Name (Get-Item $DatabaseBackupPath).FullName -ErrorAction Stop; 565 | invokeSql -Query:"ALTER DATABASE [$Name] SET RECOVERY SIMPLE" -Database:master 566 | } 567 | 568 | $ObjectQualifier = $webConfig.configuration.dotnetnuke.data.providers.add.objectQualifier.TrimEnd('_') 569 | $DatabaseOwner = $webConfig.configuration.dotnetnuke.data.providers.add.databaseOwner.TrimEnd('.') 570 | 571 | if ($PSCmdlet.ShouldProcess($Name, 'Update Portal Aliases')) { 572 | $aliases = @(invokeSql -Query:"SELECT PortalID, HTTPAlias FROM $(getDnnDatabaseObjectName -objectName:'PortalAlias' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) ORDER BY PortalID, HTTPAlias" -Database:$Name); 573 | if ($Domain -ne '') { 574 | $aliasCount = $aliases.Count 575 | $ProcessManual = $false 576 | 577 | if ($Interactive.IsPresent -and $aliasCount -gt 0) { 578 | $ProcessManual = Read-BooleanChoice -caption:'Manually Rename Portal Aliases' -message:"Would you like to specify new HTTP aliases for all $aliasCount aliases?" -defaultChoice:$true 579 | } 580 | 581 | foreach ($aliasRow in $aliases) { 582 | $alias = $aliasRow.HTTPAlias 583 | Write-Verbose "Updating $alias" 584 | $newAlias = renameAlias -Domain:$Domain -Name:$Name -Alias:$alias -NameExtension:$NameExtension; 585 | if ($ProcessManual) { 586 | $customAlias = Read-Host -Prompt "New name for Portal ID $($aliasRow.PortalID) alias: '$alias' (default: $newAlias)" 587 | $newAlias = if ($customAlias) { $customAlias } else { $newAlias } 588 | } 589 | if ($PSCmdlet.ShouldProcess($newAlias, 'Rename alias')) { 590 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'PortalAlias' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET HTTPAlias = '$newAlias' WHERE HTTPAlias = '$alias'" -Database:$Name 591 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'PortalSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = '$newAlias' WHERE SettingName = 'DefaultPortalAlias' AND SettingValue = '$alias'" -Database:$Name 592 | } 593 | } 594 | } 595 | 596 | $aliases = invokeSql -Query:"SELECT HTTPAlias FROM $(getDnnDatabaseObjectName -objectName:'PortalAlias' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) ORDER BY PortalID, HTTPAlias" -Database:$Name 597 | foreach ($aliasRow in $aliases) { 598 | $aliasInfo = readPortalAlias -Alias:$aliasRow.HTTPAlias; 599 | $aliasHost = $aliasInfo.host; 600 | $port = $aliasInfo.port; 601 | 602 | $existingBinding = Get-IISSiteBinding -Name:$Name -BindingInformation:"*:$($port):$aliasHost" -Protocol:http 603 | if ($null -eq $existingBinding) { 604 | Write-Verbose "Setting up IIS binding and HOSTS entry for $aliasHost" 605 | if ($PSCmdlet.ShouldProcess($aliasHost, 'Create IIS Site Binding')) { 606 | New-IISSiteBinding -Name:$Name -BindingInformation:"*:$($port):$aliasHost" -Protocol:http; 607 | } 608 | if ($PSCmdlet.ShouldProcess($aliasHost, 'Add HOSTS file entry')) { 609 | Add-HostFileEntry $aliasHost -WhatIf:$WhatIfPreference -Confirm:$false; 610 | } 611 | } 612 | else { 613 | Write-Verbose "IIS binding already exists for $aliasHost" 614 | } 615 | 616 | $domains.Add($aliasHost) 617 | } 618 | } 619 | 620 | if ($ObjectQualifier -ne '') { 621 | $oq = $ObjectQualifier + '_' 622 | } 623 | else { 624 | $oq = '' 625 | } 626 | 627 | $sqlPath = Join-Path 'SQLSERVER:' 'SQL'; 628 | $localhostSqlPath = Join-Path $sqlPath '(local)'; 629 | $localSqlPath = Join-Path $localhostSqlPath 'DEFAULT'; 630 | $databasesPath = Join-Path $localSqlPath 'Databases'; 631 | $databasePath = Join-Path $databasesPath (ConvertTo-EncodedSqlName $Name); 632 | $tablesPath = Join-Path $databasePath 'Tables'; 633 | $catalookSettingsTablePath = Join-Path $tablesPath "$DatabaseOwner.${oq}CAT_Settings"; 634 | if ((Test-Path $catalookSettingsTablePath) -and ($PSCmdlet.ShouldProcess($Name, 'Set Catalook to test mode'))) { 635 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'CAT_Settings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET PostItems = 0, StorePaymentTypes = 32, StoreCCTypes = 23, CCLogin = '${env:CatalookTestCCLogin}', CCPassword = '${env:CatalookTestCCPassword}', CCMerchantHash = '${env:CatalookTestCCMerchantHash}', StoreCurrencyid = 2, CCPaymentProcessorID = 59, LicenceKey = '${env:CatalookTestLicenseKey}', StoreEmail = '${env:CatalookTestStoreEmail}', Skin = '${env:CatalookTestSkin}', EmailTemplatePackage = '${env:CatalookTestEmailTemplatePackage}', CCTestMode = 1, EnableAJAX = 1" -Database:$Name 636 | } 637 | 638 | $esmSettingsTablePath = Join-Path $tablesPath "$DatabaseOwner.${oq}esm_Settings"; 639 | $esmSettingsColumnsPath = Join-Path $esmSettingsTablePath 'Columns'; 640 | $esmSettingsFattmerchantPath = Join-Path $esmSettingsColumnsPath 'FattmerchantMerchantId'; 641 | if ((Test-Path $esmSettingsFattmerchantPath) -and ($PSCmdlet.ShouldProcess($Name, 'Set FattMerchant to test mode'))) { 642 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'esm_Settings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET MerchantRegistrationStatusId = null, FattmerchantMerchantId = null, FattmerchantApiKey = '${env:FattmerchantTestApiKey}', FattmerchantPaymentsToken = '${env:FattmerchantTestPaymentsToken}' WHERE CCPaymentProcessorID = 185" -Database:$Name 643 | } 644 | 645 | $esmSettingsStaxPath = Join-Path $esmSettingsColumnsPath 'StaxMerchantId'; 646 | if ((Test-Path $esmSettingsStaxPath) -and ($PSCmdlet.ShouldProcess($Name, 'Set Stax to test mode'))) { 647 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'esm_Settings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET MerchantRegistrationStatusId = null, StaxMerchantId = null, StaxApiKey = '${env:StaxTestApiKey}', StaxPaymentsToken = '${env:StaxTestPaymentsToken}' WHERE CCPaymentProcessorID = 185" -Database:$Name 648 | } 649 | 650 | $esmParticipantTablePath = Join-Path $tablesPath "$DatabaseOwner.${oq}esm_Participant"; 651 | if ((Test-Path $esmParticipantTablePath) -and ($PSCmdlet.ShouldProcess($Name, 'Turn off payment processing for Engage: AMS'))) { 652 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'esm_Participant' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET PaymentProcessorCustomerId = NULL" -Database:$Name 653 | } 654 | 655 | if ($PSCmdlet.ShouldProcess($Name, 'Turn off SMTP for Mandeeps Live Campaign')) { 656 | $liveCampaignSettingTablePath = Join-Path $tablesPath "$DatabaseOwner.${oq}LiveCampaign_Setting"; 657 | if (Test-Path $liveCampaignSettingTablePath) { 658 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'LiveCampaign_Setting' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SMTPServerMode = 'DNNHostSettings', SendGridAPI = NULL WHERE SMTPServerMode = 'Sendgrid'" -Database:$Name 659 | } 660 | 661 | $liveCampaignSmtpTablePath = Join-Path $tablesPath "$DatabaseOwner.${oq}LiveCampaign_SmtpServer"; 662 | if (Test-Path $liveCampaignSmtpTablePath) { 663 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'LiveCampaign_SmtpServer' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET Server = 'localhost', Username = '', Password = ''" -Database:$Name 664 | } 665 | } 666 | 667 | $desktopModulePath = Join-Path $websitePath 'DesktopModules'; 668 | $engageSportsPath = Join-Path $desktopModulePath 'EngageSports'; 669 | if ((Test-Path $engageSportsPath) -and ($PSCmdlet.ShouldProcess($Name, 'Update Engage: Sports wizard URLs'))) { 670 | updateWizardUrls $Name 671 | } 672 | 673 | Write-Information "Setting SMTP to localhost" 674 | if ($PSCmdlet.ShouldProcess($Name, 'Set SMTP to localhost')) { 675 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'HostSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = 'localhost' WHERE SettingName = 'SMTPServer'" -Database:$Name 676 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'HostSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = '0' WHERE SettingName = 'SMTPAuthentication'" -Database:$Name 677 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'HostSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = 'N' WHERE SettingName = 'SMTPEnableSSL'" -Database:$Name 678 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'HostSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = '' WHERE SettingName = 'SMTPUsername'" -Database:$Name 679 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'HostSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = '' WHERE SettingName = 'SMTPPassword'" -Database:$Name 680 | 681 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'PortalSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = 'localhost' WHERE SettingName = 'SMTPServer'" -Database:$Name 682 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'PortalSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = '0' WHERE SettingName = 'SMTPAuthentication'" -Database:$Name 683 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'PortalSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = 'N' WHERE SettingName = 'SMTPEnableSSL'" -Database:$Name 684 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'PortalSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = '' WHERE SettingName = 'SMTPUsername'" -Database:$Name 685 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'PortalSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = '' WHERE SettingName = 'SMTPPassword'" -Database:$Name 686 | } 687 | 688 | if ($PSCmdlet.ShouldProcess($Name, 'Clear WebServers table')) { 689 | invokeSql -Query:"TRUNCATE TABLE $(getDnnDatabaseObjectName -objectName:'WebServers' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier)" -Database:$Name 690 | } 691 | 692 | if ($PSCmdlet.ShouldProcess($Name, 'Turn off event log buffer')) { 693 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'HostSettings' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET SettingValue = 'N' WHERE SettingName = 'EventLogBuffer'" -Database:$Name 694 | } 695 | 696 | if ($PSCmdlet.ShouldProcess($Name, 'Turn off search crawler')) { 697 | invokeSql -Query:"UPDATE $(getDnnDatabaseObjectName -objectName:'Schedule' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) SET Enabled = 0 WHERE TypeFullName = 'DotNetNuke.Professional.SearchCrawler.SearchSpider.SearchSpider, DotNetNuke.Professional.SearchCrawler'" -Database:$Name 698 | } 699 | 700 | if ($PSCmdlet.ShouldProcess($Name, "Set all passwords to 'pass'")) { 701 | invokeSql -Query:"UPDATE aspnet_Membership SET PasswordFormat = 0, Password = 'pass'" -Database:$Name 702 | } 703 | 704 | if ($PSCmdlet.ShouldProcess($Name, 'Watermark site logo(s)')) { 705 | watermarkLogos $Name $NameExtension 706 | } 707 | 708 | $appInsightsPath = Join-Path $websitePath 'ApplicationInsights.config'; 709 | if ((Test-Path $appInsightsPath) -and ($PSCmdlet.ShouldProcess($Name, 'Remove Application Insights config'))) { 710 | Remove-Item $appInsightsPath -WhatIf:$WhatIfPreference -Confirm:$false; 711 | } 712 | } 713 | 714 | if ($PSCmdlet.ShouldProcess($webConfigPath, 'Set connectionString in web.config')) { 715 | $connectionString = "Data Source=.`;Initial Catalog=$Name`;Integrated Security=true" 716 | $webConfig.configuration.connectionStrings.add | Where-Object { $_.name -eq 'SiteSqlServer' } | ForEach-Object { $_.connectionString = $connectionString } 717 | $webConfig.configuration.appSettings.add | Where-Object { $_.key -eq 'SiteSqlServer' } | ForEach-Object { $_.value = $connectionString } 718 | $webConfig.Save($webConfigPath) 719 | } 720 | 721 | if ($PSCmdlet.ShouldProcess($webConfigPath, 'Set objectQualifier and databaseOwner in web.config')) { 722 | $webConfig.configuration.dotnetnuke.data.providers.add | Where-Object { $_.name -eq 'SqlDataProvider' } | ForEach-Object { $_.objectQualifier = $ObjectQualifier; $_.databaseOwner = $DatabaseOwner } 723 | $webConfig.Save($webConfigPath) 724 | } 725 | 726 | $systemWebSection = $webConfig.configuration['system.web']; 727 | if (-not $systemWebSection) { 728 | $systemWebSection = $webConfig.configuration.location['system.web']; 729 | } 730 | 731 | if ($PSCmdlet.ShouldProcess($webConfigPath, 'Update web.config to allow short passwords')) { 732 | $systemWebSection.membership.providers.add | Where-Object { $_.type -eq 'System.Web.Security.SqlMembershipProvider' } | ForEach-Object { $_.minRequiredPasswordLength = '4' } 733 | $webConfig.Save($webConfigPath) 734 | } 735 | 736 | if ($PSCmdlet.ShouldProcess($webConfigPath, 'Turn on debug mode in web.config')) { 737 | $systemWebSection.compilation.debug = 'true' 738 | $webConfig.Save($webConfigPath) 739 | } 740 | 741 | $loginName = "IIS AppPool\$Name"; 742 | $sqlPath = Join-Path 'SQLSERVER:' 'SQL'; 743 | $localhostSqlPath = Join-Path $sqlPath '(local)'; 744 | $localSqlPath = Join-Path $localhostSqlPath 'DEFAULT'; 745 | $loginsPath = Join-Path $localSqlPath 'Logins'; 746 | $loginPath = Join-Path $loginsPath (ConvertTo-EncodedSqlName $loginName); 747 | if ((-not (Test-Path $loginPath)) -and ($PSCmdlet.ShouldProcess($loginName, 'Create SQL Server login'))) { 748 | invokeSql -Query:"CREATE LOGIN [$loginName] FROM WINDOWS WITH DEFAULT_DATABASE = [$Name];" -Database:master 749 | } 750 | 751 | if ($PSCmdlet.ShouldProcess($loginName, 'Create SQL Server User')) { 752 | invokeSql -Query:"CREATE USER [$loginName] FOR LOGIN [$loginName];" -Database:$Name 753 | } 754 | if ($PSCmdlet.ShouldProcess($loginName, 'Add db_owner role')) { 755 | invokeSql -Query:"EXEC sp_addrolemember N'db_owner', N'$loginName';" -Database:$Name 756 | } 757 | 758 | if ($PSCmdlet.ShouldProcess($Name, 'Add HTTPS bindings')) { 759 | New-SslWebBinding $Name $domains -WhatIf:$WhatIfPreference -Confirm:$false; 760 | } 761 | 762 | if ($PSCmdlet.ShouldProcess("https://$Name", 'Open browser')) { 763 | if (Get-Command -Name:Start-Process -ParameterName:WhatIf -ErrorAction SilentlyContinue) { 764 | Start-Process -FilePath:https://$Name -WhatIf:$WhatIfPreference -Confirm:$false 765 | } 766 | else { 767 | Start-Process -FilePath:https://$Name; 768 | } 769 | } 770 | 771 | <# 772 | .SYNOPSIS 773 | Creates a DNN site 774 | .DESCRIPTION 775 | Creates a DNN site, either from a file system zip and database backup, or a new installation 776 | .PARAMETER Name 777 | The name of the site (the domain, folder name, and database name, e.g. dnn.local) 778 | .PARAMETER SiteZipPath 779 | The full path to the DNN site zip file or directory 780 | .PARAMETER ObjectQualifier 781 | The database object qualifier 782 | .PARAMETER DatabaseOwner 783 | The database schema 784 | .PARAMETER DatabaseBackupPath 785 | The full path to the database backup (.bak file). This must be in a location to which SQL Server has access 786 | .PARAMETER Domain 787 | If specified, the Portal Alias table will be updated to replace the old site domain with the new site domain 788 | .PARAMETER GitRepository 789 | If specified, the git repository at the given URL/path will be cloned into the site's folder 790 | .PARAMETER Interactive 791 | Whether the cmdlet can prompt the user for additional information (e.g. how to rename additional portal aliases) 792 | #> 793 | } 794 | 795 | function readPortalAlias($alias) { 796 | if ($alias -Like '*/*') { 797 | $split = $alias.Split('/'); 798 | $aliasHost = $split[0]; 799 | $childAlias = $split[1..($split.length - 1)] -join '/'; 800 | } 801 | else { 802 | $aliasHost = $alias; 803 | $childAlias = $null; 804 | } 805 | 806 | if ($aliasHost -Like '*:*') { 807 | $split = $aliasHost.Split(':'); 808 | $aliasHost = $split[0]; 809 | $port = $split[1]; 810 | } 811 | else { 812 | $port = 80; 813 | } 814 | return [pscustomobject]@{host = $aliasHost; port = $port; childAlias = $childAlias }; 815 | } 816 | 817 | function renameAlias([string]$Domain, [string]$Name, [string]$Alias, [string]$NameExtension) { 818 | $newAlias = $Alias -replace $Domain, $Name; 819 | $aliasInfo = readPortalAlias($newAlias); 820 | $aliasHost = $aliasInfo.host; 821 | 822 | if ($aliasHost -NotLike "*$Name*") { 823 | $aliasHost = $aliasHost + $NameExtension; 824 | $newAlias = $aliasHost; 825 | if ($aliasInfo.port -ne 80) { 826 | $newAlias = $newAlias + ':' + $aliasInfo.port; 827 | } 828 | 829 | if ($aliasInfo.childAlias) { 830 | $newAlias = $newAlias + '/' + $aliasInfo.childAlias; 831 | } 832 | } 833 | 834 | return $newAlias; 835 | } 836 | 837 | function extractZip { 838 | param( 839 | [parameter(Mandatory = $true, position = 0)] 840 | [string]$output, 841 | [parameter(Mandatory = $true, position = 1)] 842 | [string]$zipFile 843 | ); 844 | 845 | Write-Verbose "extracting from $zipFile to $output" 846 | if (Get-Command '7zG' -ErrorAction SilentlyContinue) { 847 | $commandName = '7zG' 848 | } 849 | elseif (Get-Command '7za' -ErrorAction SilentlyContinue) { 850 | $commandName = '7za' 851 | } 852 | else { 853 | $commandName = $false 854 | } 855 | if ($commandName) { 856 | try { 857 | $outputFile = [System.IO.Path]::GetTempFileName() 858 | if (Get-Command -Name:Start-Process -ParameterName:WhatIf -ErrorAction SilentlyContinue) { 859 | $process = Start-Process $commandName -ArgumentList "x -y -o`"$output`" -- `"$zipFile`"" -Wait -NoNewWindow -PassThru -RedirectStandardOutput $outputFile -WhatIf:$WhatIfPreference -Confirm:$false; 860 | } 861 | else { 862 | $process = Start-Process $commandName -ArgumentList "x -y -o`"$output`" -- `"$zipFile`"" -Wait -NoNewWindow -PassThru -RedirectStandardOutput $outputFile; 863 | } 864 | 865 | if ($process.ExitCode -ne 0) { 866 | $zipLogOutput = Get-Content $outputFile; 867 | if ($zipLogOutput) { 868 | Write-Warning $zipLogOutput 869 | } 870 | 871 | if ($process.ExitCode -eq 1) { 872 | if ($zipLogOutput) { 873 | Write-Warning "Non-fatal error extracting $zipFile, see above 7-Zip output" 874 | } 875 | else { 876 | Write-Warning "Non-fatal error extracting $zipFile" 877 | } 878 | } 879 | else { 880 | if ($zipLogOutput) { 881 | Write-Error "Error extracting $zipFile, see above 7-Zip output" 882 | } 883 | else { 884 | Write-Error "Error extracting $zipFile" 885 | } 886 | } 887 | } 888 | } 889 | finally { 890 | Remove-Item $outputFile -WhatIf:$false -Confirm:$false; 891 | } 892 | } 893 | else { 894 | Write-Verbose 'Couldn''t find 7-Zip (try running ''choco install 7zip.commandline''), expanding with the (slower) Expand-Archive cmdlet' 895 | if (-not (Test-Path $output)) { 896 | mkdir $output | Out-Null 897 | } 898 | Expand-Archive $zipFile -DestinationPath $output 899 | } 900 | } 901 | 902 | function extractPackages { 903 | param( 904 | [parameter(Mandatory = $true, position = 0)] 905 | [string]$Name, 906 | [parameter(Mandatory = $true, position = 1)] 907 | [string]$SiteZipPath 908 | ); 909 | 910 | $SiteZipOutputPath = $null; 911 | $CopyEntireDirectory = $false; 912 | $sitePath = Join-Path $www $Name; 913 | $websitePath = Join-Path $sitePath 'Website'; 914 | $websiteExtractPath = Join-Path $sitePath 'Extracted_Website'; 915 | 916 | $SiteZipPath = $SiteZipPath.Trim("/\"); 917 | if ($SiteZipPath -ne '') { 918 | if (Test-Path $SiteZipPath -PathType Leaf) { 919 | if (Test-Path $sitePath -PathType Container) { 920 | $SiteZipOutputPath = $websiteExtractPath; 921 | } 922 | else { 923 | $SiteZipOutputPath = $websitePath; 924 | } 925 | 926 | extractZip $SiteZipOutputPath $SiteZipPath; 927 | $SiteZipPath = $SiteZipOutputPath 928 | 929 | $unzippedFiles = @(Get-ChildItem $SiteZipOutputPath -Directory) 930 | if ($unzippedFiles.Length -eq 1) { 931 | Write-Verbose "Found a single folder in the zip, assuming it's the entire website" 932 | $SiteZipPath = Join-Path $SiteZipPath $unzippedFiles.Name; 933 | } 934 | } 935 | 936 | $binPath = Join-Path $SiteZipPath 'bin' 937 | $assemblyPath = Join-Path $binPath 'DotNetNuke.dll'; 938 | if (-not (Test-Path $assemblyPath)) { 939 | $websitePath = Join-Path $SiteZipPath 'Website'; 940 | $binPath = Join-Path $websitePath 'bin'; 941 | $assemblyPath = Join-Path $binPath 'DotNetNuke.dll'; 942 | if (Test-Path $assemblyPath) { 943 | $CopyEntireDirectory = Test-Path (Join-Path $SiteZipPath '.gitignore'); 944 | if (-not $CopyEntireDirectory) { 945 | $SiteZipPath = Join-Path $SiteZipPath "Website" 946 | Write-Verbose "Found a Website folder, adjusting path" 947 | } 948 | else { 949 | Write-Verbose "Found a .gitignore file, assuming this is a development site" 950 | } 951 | } 952 | } 953 | } 954 | 955 | if ($null -eq $SiteZipPath -or $SiteZipPath -eq '' -or -not (Test-Path $SiteZipPath)) { 956 | throw "The supplied file $SiteZipPath could not be found, aborting installation" 957 | } 958 | 959 | $SiteZipPath = (Get-Item $SiteZipPath).FullName 960 | Write-Information "Extracting DNN site" 961 | if (Test-Path $SiteZipPath -PathType Leaf) { 962 | if (Test-Path $sitePath -PathType Container) { 963 | $SiteZipOutputPath = $websiteExtractPath; 964 | } 965 | else { 966 | $SiteZipOutputPath = $websitePath; 967 | } 968 | 969 | extractZip $SiteZipOutputPath $SiteZipPath 970 | $SiteZipPath = $SiteZipOutputPath 971 | } 972 | 973 | if ($SiteZipPath -eq $sitePath -or $SiteZipPath -eq $websitePath) { 974 | return; 975 | } 976 | 977 | if ($CopyEntireDirectory) { 978 | $to = $sitePath 979 | } 980 | else { 981 | $to = $websitePath 982 | } 983 | $from = $SiteZipPath 984 | 985 | 986 | if ($SiteZipOutputPath) { 987 | moveWithProgress -from:$from -to:$to; 988 | Remove-Item $from -Force -Recurse -Confirm:$false; 989 | } 990 | else { 991 | copyWithProgress -from:$from -to:$to; 992 | } 993 | } 994 | 995 | function copyWithProgress($from, $to) { 996 | processFilesWithProgress ` 997 | -from:$from ` 998 | -to:$to ` 999 | -process: { param($source, $destination); Copy-Item $source $destination -Force -Confirm:$false; } ` 1000 | -activity:"Copying files to $to" ` 1001 | -status:'Copying…'; 1002 | } 1003 | 1004 | function moveWithProgress($from, $to) { 1005 | processFilesWithProgress ` 1006 | -from:$from ` 1007 | -to:$to ` 1008 | -process: { param($source, $destination); Move-Item $source $destination -Force -Confirm:$false; } ` 1009 | -activity:"Moving files to $to" ` 1010 | -status:'Moving…'; 1011 | } 1012 | 1013 | function processFilesWithProgress($from, $to, [scriptblock]$process, $activity, $status) { 1014 | $filesToCopy = Get-ChildItem $from -Recurse -File -Force; 1015 | $totalCount = $filesToCopy.Count; 1016 | $progressCount = 0; 1017 | Write-Progress -Activity:$activity -Status:$status -PercentComplete 0; 1018 | foreach ($file in $filesToCopy) { 1019 | $progressCount += 1; 1020 | $relativePath = $file.FullName -replace [regex]::escape($from), ''; 1021 | $destination = Join-Path $to $relativePath; 1022 | Write-Progress -Activity:$activity -Status:$status -PercentComplete ($progressCount / $totalCount * 100) -CurrentOperation:$destination; 1023 | $directory = Split-Path $destination; 1024 | $baseDirectory = Split-Path $directory; 1025 | $directoryName = Split-Path $directory -Leaf; 1026 | New-Item -Path:$baseDirectory -Name:$directoryName -ItemType:Directory -Force -Confirm:$false | Out-Null; 1027 | Invoke-Command -ScriptBlock:$process -ArgumentList:@($file.FullName, $destination); 1028 | } 1029 | 1030 | Write-Progress -Activity:$activity -PercentComplete 100 -Completed; 1031 | } 1032 | 1033 | $sqlModuleHasEncryptParam = $null -ne (Get-Command Invoke-Sqlcmd).Parameters['Encrypt']; 1034 | function invokeSql { 1035 | param( 1036 | [parameter(Mandatory = $true, position = 0)] 1037 | [string]$Query, 1038 | [parameter(Mandatory = $true, position = 1)] 1039 | [string]$Database 1040 | ); 1041 | 1042 | if ($sqlModuleHasEncryptParam) { 1043 | Invoke-Sqlcmd -Query:$Query -Database:$Database -Encrypt:Optional; 1044 | } 1045 | else { 1046 | Invoke-Sqlcmd -Query:$Query -Database:$Database -EncryptConnection:$false; 1047 | } 1048 | } 1049 | 1050 | 1051 | function newDnnDatabase { 1052 | param( 1053 | [parameter(Mandatory = $true, position = 0)] 1054 | [string]$Name 1055 | ); 1056 | 1057 | invokeSql -Query:"CREATE DATABASE [$Name];" -Database:master; 1058 | invokeSql -Query:"ALTER DATABASE [$Name] SET RECOVERY SIMPLE;" -Database:master; 1059 | } 1060 | 1061 | function restoreDnnDatabase { 1062 | param( 1063 | [parameter(Mandatory = $true, position = 0)] 1064 | [string]$Name, 1065 | [parameter(Mandatory = $true, position = 1)] 1066 | [string]$DatabaseBackupPath 1067 | ); 1068 | 1069 | $softwareRegistryPath = Join-Path 'HKLM:' 'SOFTWARE'; 1070 | $microsoftRegistryPath = Join-Path $softwareRegistryPath 'Microsoft'; 1071 | $sqlServerRegistryPath = Join-Path $microsoftRegistryPath 'Microsoft SQL Server'; 1072 | if (Test-Path $sqlServerRegistryPath) { 1073 | $defaultInstanceKey = Get-ChildItem $sqlServerRegistryPath | Where-Object { $_.Name -match 'MSSQL\d+\.MSSQLSERVER$' } | Select-Object 1074 | if ($defaultInstanceKey) { 1075 | $defaultInstanceInfoPath = Join-Path $defaultInstanceKey.PSPath 'MSSQLServer' 1076 | $backupDir = $(Get-ItemProperty -path:$defaultInstanceInfoPath -name:BackupDirectory).BackupDirectory 1077 | if ($backupDir) { 1078 | $sqlAcl = Get-Acl $backupDir 1079 | Set-Acl $DatabaseBackupPath $sqlAcl -Confirm:$false; 1080 | } 1081 | else { 1082 | Write-Warning 'Unable to find SQL Server backup directory, backup file will not have ACL permissions set' 1083 | } 1084 | } 1085 | else { 1086 | Write-Warning 'Unable to find SQL Server info in registry, backup file will not have ACL permissions set' 1087 | } 1088 | } 1089 | else { 1090 | Write-Warning 'Unable to find SQL Server info in registry, backup file will not have ACL permissions set' 1091 | } 1092 | 1093 | $dbRestoreFile = New-Object Microsoft.SqlServer.Management.Smo.RelocateFile; 1094 | $dbRestoreLog = New-Object Microsoft.SqlServer.Management.Smo.RelocateFile; 1095 | 1096 | $logicalDataFileName = $Name; 1097 | $logicalLogFileName = $Name; 1098 | 1099 | #based on http://redmondmag.com/articles/2009/12/21/automated-restores.aspx 1100 | $server = New-Object Microsoft.SqlServer.Management.Smo.Server('(local)'); 1101 | $dbRestore = New-Object Microsoft.SqlServer.Management.Smo.Restore; 1102 | $dbRestore.Devices.AddDevice($DatabaseBackupPath, [Microsoft.SqlServer.Management.Smo.DeviceType]::File) 1103 | foreach ($file in $dbRestore.ReadFileList($server)) { 1104 | switch ($file.Type) { 1105 | 'D' { $logicalDataFileName = $file.LogicalName } 1106 | 'L' { $logicalLogFileName = $file.LogicalName } 1107 | } 1108 | } 1109 | 1110 | $dbRestoreFile.LogicalFileName = $logicalDataFileName; 1111 | $dbRestoreFile.PhysicalFileName = Join-Path $server.Information.MasterDBPath ($Name + '_Data.mdf'); 1112 | $dbRestoreLog.LogicalFileName = $logicalLogFileName; 1113 | $dbRestoreLog.PhysicalFileName = Join-Path $server.Information.MasterDBLogPath ($Name + '_Log.ldf'); 1114 | 1115 | Restore-SqlDatabase -ReplaceDatabase -Database:$Name -RelocateFile:@($dbRestoreFile, $dbRestoreLog) -BackupFile:$DatabaseBackupPath -ServerInstance:'(local)' -Confirm:$false; 1116 | } 1117 | 1118 | function getDnnDatabaseObjectName { 1119 | param( 1120 | [parameter(Mandatory = $true, position = 0)] 1121 | [string]$objectName, 1122 | [parameter(Mandatory = $true, position = 1)] 1123 | [string]$DatabaseOwner, 1124 | [parameter(Mandatory = $false, position = 2)] 1125 | [string]$ObjectQualifier 1126 | ); 1127 | 1128 | if ($ObjectQualifier -ne '') { $ObjectQualifier += '_' } 1129 | return $DatabaseOwner + ".[$ObjectQualifier$objectName]" 1130 | } 1131 | 1132 | function updateWizardUrls { 1133 | param( 1134 | [parameter(Mandatory = $true, position = 0)] 1135 | [string]$Name 1136 | ); 1137 | 1138 | $uri = $null 1139 | $sitePath = Join-Path $www $Name; 1140 | $websitePath = Join-Path $sitePath 'Website'; 1141 | $desktopModulePath = Join-Path $websitePath 'DesktopModules'; 1142 | $wizardPath = Join-Path $desktopModulePath 'EngageSports'; 1143 | foreach ($wizardManifest in (Get-ChildItem $wizardPath -Include:'*Wizard*.xml')) { 1144 | [xml]$wizardXml = Get-Content $wizardManifest 1145 | foreach ($urlNode in $wizardXml.GetElementsByTagName("NextUrl")) { 1146 | if ([System.Uri]::TryCreate([string]$urlNode.InnerText, [System.UriKind]::Absolute, [ref] $uri)) { 1147 | $urlNode.InnerText = "https://$Name" + $uri.AbsolutePath 1148 | } 1149 | } 1150 | 1151 | $wizardXml.Save($wizardManifest.FullName) 1152 | } 1153 | } 1154 | 1155 | function watermarkLogos { 1156 | param( 1157 | [parameter(Mandatory = $true, position = 0)] 1158 | [string]$Name, 1159 | [parameter(Mandatory = $true, position = 1)] 1160 | [string]$NameExtension 1161 | ); 1162 | 1163 | if (Get-Command 'gm.exe' -ErrorAction:SilentlyContinue) { 1164 | $cmd = 'gm.exe' 1165 | $subCmd = 'mogrify' 1166 | } 1167 | elseif (Get-Command 'mogrify' -ErrorAction:SilentlyContinue) { 1168 | $cmd = 'mogrify' 1169 | $subCmd = '' 1170 | } 1171 | else { 1172 | Write-Warning "Could not watermark logos, because neither GrapgicsMagick nor ImageMagick's mogrify command could not be found" 1173 | return 1174 | } 1175 | 1176 | $sitePath = Join-Path $www $Name; 1177 | $websitePath = Join-Path $sitePath 'Website'; 1178 | $logos = invokeSql -Query:"SELECT HomeDirectory + N'/' + LogoFile AS Logo FROM $(getDnnDatabaseObjectName -objectName:'Vw_Portals' -DatabaseOwner:$DatabaseOwner -ObjectQualifier:$ObjectQualifier) WHERE LogoFile IS NOT NULL" -Database:$Name 1179 | $watermarkText = $NameExtension.Substring(1) 1180 | foreach ($logo in $logos) { 1181 | $logoFile = Join-Path $websitePath $logo.Logo.Replace('/', '\'); 1182 | & $cmd $subCmd -font Arial -pointsize 60 -draw "gravity Center fill #00ff00 text 0,0 $watermarkText" -draw "gravity NorthEast fill #ff00ff text 0,0 $watermarkText" -draw "gravity SouthWest fill #00ffff text 0,0 $watermarkText" -draw "gravity NorthWest fill #ff0000 text 0,0 $watermarkText" -draw "gravity SouthEast fill #0000ff text 0,0 $watermarkText" $logoFile 1183 | } 1184 | } 1185 | 1186 | Export-ModuleMember Install-DNNResource 1187 | Export-ModuleMember Remove-DNNSite 1188 | Export-ModuleMember Rename-DNNSite 1189 | Export-ModuleMember New-DNNSite 1190 | Export-ModuleMember Update-DNNSite 1191 | Export-ModuleMember Restore-DNNSite 1192 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Engage Software 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PSScriptAnalyzerSettings.psd1: -------------------------------------------------------------------------------- 1 | # Settings for PSScriptAnalyzer invocation. 2 | # based on https://devblogs.microsoft.com/powershell/using-psscriptanalyzer-to-check-powershell-version-compatibility/ 3 | @{ 4 | Rules = @{ 5 | PSUseCompatibleCommands = @{ 6 | Enable = $true 7 | TargetProfiles = @( 8 | 'win-8_x64_10.0.17763.0_6.1.3_x64_4.0.30319.42000_core', # PS 6.1, Server 2019 9 | 'win-48_x64_10.0.17763.0_6.1.3_x64_4.0.30319.42000_core', # PS 6.1, Windows 10 10 | 'ubuntu_x64_18.04_6.1.3_x64_4.0.30319.42000_core', # PS 6.1, Ubuntu 11 | 'win-8_x64_10.0.14393.0_5.1.14393.2791_x64_4.0.30319.42000_framework', # PS 5.1, Server 2016 12 | 'win-48_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework', # PS 5.1, Windows 10 13 | 'win-8_x64_6.3.9600.0_4.0_x64_4.0.30319.42000_framework', # PS 4.0, Server 2012R2 14 | 'win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework' # PS 3.0, Server 2012 15 | ) 16 | } 17 | PSUseCompatibleSyntax = @{ 18 | Enable = $true 19 | TargetVersions = @( 20 | '6.1', 21 | '5.1', 22 | '4.0', 23 | '3.0' 24 | ) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerShellModules 2 | 3 | [![Build Status](https://dev.azure.com/dukesb11/PowershellModules/_apis/build/status/bdukes.PowerShellModules?branchName=main)](https://dev.azure.com/dukesb11/PowershellModules/_build/latest?definitionId=2&branchName=main) 4 | 5 | A collection of PowerShell modules 6 | 7 | - [`ACL-Permissions` ![PowerShell Gallery: ACL-Permissions platform support](https://img.shields.io/powershellgallery/p/ACL-Permissions)![PowerShell Gallery: ACL-Permissions download count](https://img.shields.io/powershellgallery/dt/ACL-Permissions)![PowerShell Gallery: ACL-Permissions version](https://img.shields.io/powershellgallery/v/ACL-Permissions?include_prereleases)](https://www.powershellgallery.com/packages/ACL-Permissions/) 8 | - [`Add-HostFileEntry` ![PowerShell Gallery: Add-HostFileEntry platform support](https://img.shields.io/powershellgallery/p/Add-HostFileEntry)![PowerShell Gallery: Add-HostFileEntry download count](https://img.shields.io/powershellgallery/dt/Add-HostFileEntry)![PowerShell Gallery: Add-HostFileEntry version](https://img.shields.io/powershellgallery/v/Add-HostFileEntry?include_prereleases)](https://www.powershellgallery.com/packages/Add-HostFileEntry/) 9 | - [`AdministratorRole` ![PowerShell Gallery: AdministratorRole platform support](https://img.shields.io/powershellgallery/p/AdministratorRole)![PowerShell Gallery: AdministratorRole download count](https://img.shields.io/powershellgallery/dt/AdministratorRole)![PowerShell Gallery: AdministratorRole version](https://img.shields.io/powershellgallery/v/AdministratorRole?include_prereleases)](https://www.powershellgallery.com/packages/AdministratorRole/) 10 | - [`BindingRedirects` ![PowerShell Gallery: BindingRedirects platform support](https://img.shields.io/powershellgallery/p/BindingRedirects)![PowerShell Gallery: BindingRedirects download count](https://img.shields.io/powershellgallery/dt/BindingRedirects)![PowerShell Gallery: BindingRedirects version](https://img.shields.io/powershellgallery/v/BindingRedirects?include_prereleases)](https://www.powershellgallery.com/packages/BindingRedirects/) 11 | - [`DnnWebsiteManagement` ![PowerShell Gallery: DnnWebsiteManagement platform support](https://img.shields.io/powershellgallery/p/DnnWebsiteManagement)![PowerShell Gallery: DnnWebsiteManagement download count](https://img.shields.io/powershellgallery/dt/DnnWebsiteManagement)![PowerShell Gallery: DnnWebsiteManagement version](https://img.shields.io/powershellgallery/v/DnnWebsiteManagement?include_prereleases)](https://www.powershellgallery.com/packages/DnnWebsiteManagement/) 12 | - [`Read-Choice` ![PowerShell Gallery: Read-Choice platform support](https://img.shields.io/powershellgallery/p/Read-Choice)![PowerShell Gallery: Read-Choice download count](https://img.shields.io/powershellgallery/dt/Read-Choice)![PowerShell Gallery: Read-Choice version](https://img.shields.io/powershellgallery/v/Read-Choice?include_prereleases)](https://www.powershellgallery.com/packages/Read-Choice/) 13 | - [`SslWebBinding` ![PowerShell Gallery: SslWebBinding platform support](https://img.shields.io/powershellgallery/p/SslWebBinding)![PowerShell Gallery: SslWebBinding download count](https://img.shields.io/powershellgallery/dt/SslWebBinding)![PowerShell Gallery: SslWebBinding version](https://img.shields.io/powershellgallery/v/SslWebBinding?include_prereleases)](https://www.powershellgallery.com/packages/SslWebBinding/) 14 | - [`Write-HtmlNode` ![PowerShell Gallery: Write-HtmlNode platform support](https://img.shields.io/powershellgallery/p/Write-HtmlNode)![PowerShell Gallery: Write-HtmlNode download count](https://img.shields.io/powershellgallery/dt/Write-HtmlNode)![PowerShell Gallery: Write-HtmlNode version](https://img.shields.io/powershellgallery/v/Write-HtmlNode?include_prereleases)](https://www.powershellgallery.com/packages/Write-HtmlNode/) 15 | - `Recycle` module is [currently delisted](https://github.com/bdukes/PowerShellModules/issues/29) 16 | -------------------------------------------------------------------------------- /Read-Choice/Read-Choice.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'Read-Choice' 3 | # 4 | # Generated by: Brian Dukes 5 | # 6 | # Generated on: 10/10/2016 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'Read-Choice.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.0.2' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'ebab63fa-f63d-427a-99c8-8450974b257c' 22 | 23 | # Author of this module 24 | Author = 'Brian Dukes' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Engage Software' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2017 Engage Software' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Prompts the user to pick a choice from a set of options. Based on http://scriptolog.blogspot.com/2007/09/make-choice.html' 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | # PowerShellVersion = '' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = @('Read-Choice', 'Read-BooleanChoice') 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = @() 76 | 77 | # Variables to export from this module 78 | VariablesToExport = @() 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = @() 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | FileList = 'Read-Choice.psm1' 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | Tags = @('PSEdition_Core', 'PSEdition_Desktop', 'Windows', 'Linux', 'macOS') 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'https://github.com/bdukes/PowerShellModules/blob/main/LICENSE' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/bdukes/PowerShellModules' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | ReleaseNotes = 'https://github.com/bdukes/PowerShellModules/blob/main/CHANGES.md' 111 | 112 | } # End of PSData hashtable 113 | 114 | } # End of PrivateData hashtable 115 | 116 | # HelpInfo URI of this module 117 | # HelpInfoURI = '' 118 | 119 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 120 | # DefaultCommandPrefix = '' 121 | 122 | } 123 | 124 | -------------------------------------------------------------------------------- /Read-Choice/Read-Choice.psm1: -------------------------------------------------------------------------------- 1 | Set-StrictMode -Version Latest 2 | 3 | function Read-Choice { 4 | param( 5 | [Parameter(Mandatory = $true)][string]$caption, 6 | [Parameter(Mandatory = $true)][string]$message, 7 | [Parameter(Mandatory = $true)][array]$choices, 8 | [int]$defaultChoiceIndex = -1 9 | ); 10 | 11 | if ($choices[0] -is [string]) { 12 | $choices = $choices | ForEach-Object { New-Object System.Management.Automation.Host.ChoiceDescription $_ } 13 | } 14 | 15 | $answerIndex = $host.ui.PromptForChoice($caption, $message, $choices, $defaultChoiceIndex) 16 | 17 | return $choices[$answerIndex].Label 18 | <# 19 | .SYNOPSIS 20 | Prompts the user to pick a choice from a set of options 21 | .DESCRIPTION 22 | Prompts the user to pick a choice from a set of options. Based on http://scriptolog.blogspot.com/2007/09/make-choice.html 23 | .PARAMETER caption 24 | The title of the prompt 25 | .PARAMETER message 26 | The question being asked 27 | .PARAMETER choices 28 | An array of choices. These can be strings or System.Management.Automation.Host.ChoiceDescription objects. Prepend a letter with an ampersand to indicate the choice's hotkey 29 | .PARAMETER defaultChoiceIndex 30 | The zero-based index of the default choice 31 | .OUTPUTS 32 | The text of the choice 33 | #> 34 | } 35 | 36 | function Read-BooleanChoice { 37 | param( 38 | [Parameter(Mandatory = $true)][string]$caption, 39 | [Parameter(Mandatory = $true)][string]$message, 40 | [string]$trueLabel = '&Yes', 41 | [string]$trueHelp = '', 42 | [string]$falseLabel = '&No', 43 | [string]$falseHelp = '', 44 | [switch]$showFalseAsFirstOption, 45 | $defaultChoice = $null 46 | ); 47 | 48 | $trueChoice = New-Object System.Management.Automation.Host.ChoiceDescription $trueLabel, $trueHelp 49 | $falseChoice = New-Object System.Management.Automation.Host.ChoiceDescription $falseLabel, $falseHelp 50 | $defaultChoiceIndex = -1 51 | if ($null -ne $defaultChoice) { 52 | $defaultChoiceIndex = 0 53 | if ($defaultChoice -eq $false -xor $showFalseAsFirstOption) { 54 | $defaultChoiceIndex = 1 55 | } 56 | } 57 | 58 | if ($showFalseAsFirstOption) { 59 | $choices = @($falseChoice, $trueChoice) 60 | } 61 | else { 62 | $choices = @($trueChoice, $falseChoice) 63 | } 64 | $answerLabel = Read-Choice -caption:$caption -message:$message -choices:$choices -defaultChoiceIndex:$defaultChoiceIndex 65 | return $answerLabel -eq $trueLabel 66 | <# 67 | .SYNOPSIS 68 | Prompts the user between two choices 69 | .DESCRIPTION 70 | Prompts the user to choose an answer to a yes-or-no question 71 | .PARAMETER caption 72 | The title of the prompt 73 | .PARAMETER trueLabel 74 | The label for the choice that evaluates as true. Use an ampersand before the letter that should be the hotkey, e.g. '&Yes' 75 | .PARAMETER trueHelp 76 | The help text for the choice that evaluates as true 77 | .PARAMETER falseLabel 78 | The label for the choice that evaluates as false. Use an ampersand before the letter that should be the hotkey, e.g. '&No' 79 | .PARAMETER falseHelp 80 | The help text for the choice that evaluates as false 81 | .PARAMETER showFalseAsFirstOption 82 | Whether to show the false choice first 83 | .PARAMETER defaultChoice 84 | $true to default to the true choice, $false to default to the false choice, or $null to have no default 85 | .OUTPUTS 86 | $true if the true choice was picked, otherwise $false 87 | #> 88 | } 89 | 90 | Export-ModuleMember Read-Choice 91 | Export-ModuleMember Read-BooleanChoice -------------------------------------------------------------------------------- /Recycle/Recycle.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'Recycle' 3 | # 4 | # Generated by: Brian Dukes 5 | # 6 | # Generated on: 8/31/2016 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'Recycle.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.5.0' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'c748f79e-539b-4ea7-866b-d91eb16a5250' 22 | 23 | # Author of this module 24 | Author = 'Brian Dukes' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Engage Software' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2022 Engage Software' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Deletes the file or folder as if it had been done via File Explorer, or restores a deleted file from the recycle bin.' 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | # PowerShellVersion = '' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = 'Remove-ItemSafely', 'Restore-RecycledItem', 'Get-RecycledItem' 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = @() 76 | 77 | # Variables to export from this module 78 | VariablesToExport = @() 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = @() 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | FileList = 'Recycle.psm1' 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | Tags = @('PSEdition_Core', 'PSEdition_Desktop', 'Windows') 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'https://github.com/bdukes/PowerShellModules/blob/main/LICENSE' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/bdukes/PowerShellModules' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | ReleaseNotes = 'https://github.com/bdukes/PowerShellModules/blob/main/CHANGES.md' 111 | 112 | } # End of PSData hashtable 113 | 114 | } # End of PrivateData hashtable 115 | 116 | # HelpInfo URI of this module 117 | # HelpInfoURI = '' 118 | 119 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 120 | # DefaultCommandPrefix = '' 121 | 122 | } 123 | -------------------------------------------------------------------------------- /Recycle/Recycle.psm1: -------------------------------------------------------------------------------- 1 | #Set-StrictMode -Version Latest 2 | 3 | function Remove-ItemSafely { 4 | 5 | [CmdletBinding(DefaultParameterSetName = 'Path', SupportsShouldProcess = $true, ConfirmImpact = 'Medium', SupportsTransactions = $true, HelpUri = 'http://go.microsoft.com/fwlink/?LinkID=113373')] 6 | param( 7 | [Parameter(ParameterSetName = 'Path', Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] 8 | [string[]] 9 | ${Path}, 10 | 11 | [Parameter(ParameterSetName = 'LiteralPath', Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 12 | [Alias('PSPath')] 13 | [string[]] 14 | ${LiteralPath}, 15 | 16 | [string] 17 | ${Filter}, 18 | 19 | [string[]] 20 | ${Include}, 21 | 22 | [string[]] 23 | ${Exclude}, 24 | 25 | [switch] 26 | ${Recurse}, 27 | 28 | [switch] 29 | ${Force}, 30 | 31 | [Parameter(ValueFromPipelineByPropertyName = $true)] 32 | [pscredential] 33 | [System.Management.Automation.CredentialAttribute()] 34 | ${Credential}, 35 | 36 | [switch] 37 | $DeletePermanently) 38 | 39 | 40 | begin { 41 | try { 42 | $outBuffer = $null 43 | if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { 44 | $PSBoundParameters['OutBuffer'] = 1 45 | } 46 | if ($DeletePermanently -or @($PSBoundParameters.Keys | Where-Object { @('Filter', 'Include', 'Exclude', 'Recurse', 'Force', 'Credential') -contains $_ }).Count -ge 1) { 47 | if ($PSBoundParameters['DeletePermanently']) { 48 | $PSBoundParameters.Remove('DeletePermanently') | Out-Null 49 | } 50 | 51 | $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Remove-Item', [System.Management.Automation.CommandTypes]::Cmdlet) 52 | $scriptCmd = { & $wrappedCmd @PSBoundParameters } 53 | } 54 | else { 55 | $scriptCmd = { & recycleItem @PSBoundParameters } 56 | } 57 | 58 | $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) 59 | $steppablePipeline.Begin($PSCmdlet) 60 | } 61 | catch { 62 | throw 63 | } 64 | } 65 | 66 | process { 67 | try { 68 | $steppablePipeline.Process($_) 69 | } 70 | catch { 71 | throw 72 | } 73 | } 74 | 75 | end { 76 | try { 77 | $steppablePipeline.End() 78 | } 79 | catch { 80 | throw 81 | } 82 | } 83 | <# 84 | 85 | .ForwardHelpTargetName Microsoft.PowerShell.Management\Remove-Item 86 | .ForwardHelpCategory Cmdlet 87 | 88 | #> 89 | 90 | } 91 | 92 | function recycleItem { 93 | [CmdletBinding(DefaultParameterSetName = 'Path', SupportsShouldProcess = $true, ConfirmImpact = 'Medium', SupportsTransactions = $true, HelpUri = 'http://go.microsoft.com/fwlink/?LinkID=113373')] 94 | param( 95 | [Parameter(ParameterSetName = 'Path', Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] 96 | [string[]] 97 | ${Path}, 98 | 99 | [Parameter(ParameterSetName = 'LiteralPath', Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 100 | [Alias('PSPath')] 101 | [string[]] 102 | ${LiteralPath}) 103 | 104 | process { 105 | try { 106 | if ($PSCmdlet.ParameterSetName -eq 'LiteralPath') { 107 | $items = @(Get-Item -LiteralPath:$PSBoundParameters['LiteralPath']) 108 | } 109 | else { 110 | $items = @(Get-Item -Path:$PSBoundParameters['Path']) 111 | } 112 | 113 | foreach ($item in $items) { 114 | if ($PSCmdlet.ShouldProcess($item)) { 115 | $directoryPath = Split-Path $item -Parent 116 | 117 | $shell = New-Object -ComObject "Shell.Application" 118 | $shellFolder = $shell.Namespace($directoryPath) 119 | $shellItem = $shellFolder.ParseName($item.Name) 120 | $shellItem.InvokeVerb("delete") 121 | } 122 | } 123 | } 124 | catch { 125 | throw 126 | } 127 | } 128 | } 129 | 130 | function Restore-RecycledItem { 131 | [CmdletBinding(DefaultParameterSetName = 'ManualSelection', SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] 132 | param( 133 | [Parameter(Position = 0, ParameterSetName = 'ComObject', Mandatory, ValueFromPipeline)] 134 | [System.__ComObject] 135 | $ComObject, 136 | 137 | [Parameter(Position = 0, ParameterSetName = 'ManualSelection', Mandatory)] 138 | [Parameter(Position = 0, ParameterSetName = 'Selector', Mandatory)] 139 | [ValidateNotNullOrEmpty()] 140 | [String] 141 | $OriginalPath, 142 | 143 | [Parameter(Position = 1, ParameterSetName = 'ManualSelection')] 144 | [ValidateSet('Application', 'GetFolder', 'GetLink', 'IsBrowsable', 'IsFileSystem', 'IsFolder', 'IsLink', 'ModifyDate', 'Name', 'Parent', 'Path', 'Size', 'Type')] 145 | [Alias('Criteria', 'Property')] 146 | [String] 147 | $SortingCriteria = 'ModifyDate', 148 | 149 | [Parameter(Position = 2, ParameterSetName = 'ManuelSelection')] 150 | [Alias('Desc')] 151 | [Switch] 152 | $Descending, 153 | 154 | [Parameter(Position = 1, ParameterSetName = 'Selector')] 155 | [Alias('Selector', 'Script', 'Lambda', 'Filter')] 156 | [ValidateNotNull()] 157 | [ScriptBlock] 158 | $SelectorScript, 159 | 160 | [Parameter(ParameterSetName = 'ManualSelection')] 161 | [Parameter(ParameterSetName = 'Selector')] 162 | [Parameter(ParameterSetName = 'ComObject')] 163 | [Switch] 164 | $Overwrite 165 | ) 166 | 167 | process { 168 | if ($ComObject) { 169 | $FoundItem = $ComObject 170 | $OriginalPath = $ComObject.GetFolder.Title 171 | } 172 | 173 | if ((Test-Path $OriginalPath) -and -not $Overwrite) { 174 | if ((Get-Item $OriginalPath) -is [System.IO.DirectoryInfo]) { 175 | Write-Error "Directory already exists and -Overwrite is not specified" 176 | } 177 | else { 178 | Write-Error "File already exists and -Overwrite is not specified" 179 | } 180 | } 181 | else { 182 | if ($PSCmdlet.ParameterSetName -eq "ManualSelection" -or $PSCmdlet.ParameterSetName -eq "Selector") { 183 | $BoundParametersLessOverwrite = $PSBoundParameters 184 | if ($BoundParametersLessOverwrite.ContainsKey("Overwrite")) { 185 | $BoundParametersLessOverwrite.Remove("Overwrite") | Out-Null 186 | } 187 | $FoundItem = Get-RecycledItem @PSBoundParameters -Top 1 188 | } 189 | 190 | if ($FoundItem) { 191 | # This does not seem to work, so I am doing it manually 192 | # Maybe someone can get this to work (although I don't see an advantage over the current method) 193 | #(New-Object -ComObject "Shell.Application").Namespace($BinItems[0].Path).Self().InvokeVerb("Restore") 194 | if ($Overwrite -or $PSBoundParameters['Force']) { 195 | Remove-ItemSafely $OriginalPath 196 | } 197 | Move-Item $FoundItem.Path $OriginalPath 198 | } 199 | else { 200 | Write-Error "No item in recycle bin with the specified path found" 201 | } 202 | } 203 | 204 | return Get-Item $OriginalPath -ErrorAction SilentlyContinue 205 | } 206 | 207 | <# 208 | .SYNOPSIS 209 | Restores a file from the Recycle Bin. 210 | .DESCRIPTION 211 | Finds the item(s) in the Recycle Bin with the given path, selects one based on the given selector (default is newest), and restores it to the original location. 212 | .PARAMETER OriginalPath 213 | The original path to the file to restore. 214 | .PARAMETER Overwrite 215 | Whether to overwrite the file at the path if it exists. 216 | .PARAMETER SortingCriteria 217 | How to sort the items to find which to restore. 218 | .PARAMETER Descending 219 | Whether the SortingCriteria sort should be descending or ascending. 220 | .PARAMETER SelectorScript 221 | A script block which determines which item to restore. 222 | .INPUTS 223 | System.__ComObject The result of calling Get-RecycledItem 224 | .OUTPUTS 225 | System.Object Return the item that was restored. 226 | .EXAMPLE 227 | Restore-Item "C:\TestFolder\TestFile.txt" 228 | .EXAMPLE 229 | Restore-Item "C:\TestFolder\TestFile.txt" -SortingCriteria "Size" -Descending 230 | .EXAMPLE 231 | Restore-Item "C:\TestFolder\TestFile.txt" -SelectorScript { $_.ModifyDate -eq '01.01.1970' } 232 | .NOTES 233 | Credit for this approach: https://jdhitsolutions.com/blog/powershell/7024/managing-the-recycle-bin-with-powershell/ 234 | .NOTES 235 | Author: Kevin Holtkamp, kevinholtkamp26@gmail.com 236 | LastEdit: 09.07.2022 237 | #> 238 | } 239 | 240 | function Get-RecycledItem { 241 | [CmdletBinding(DefaultParameterSetName = 'OriginalPath')] 242 | param( 243 | [Parameter(Position = 0, ValueFromPipeline, ParameterSetName = 'OriginalPath')] 244 | [String] 245 | $OriginalPath, 246 | 247 | [Parameter(Position = 1, ParameterSetName = 'OriginalPathRegex')] 248 | [String] 249 | $OriginalPathRegex, 250 | 251 | [Parameter(Position = 2)] 252 | [ValidateSet('Application', 'GetFolder', 'GetLink', 'IsBrowsable', 'IsFileSystem', 'IsFolder', 'IsLink', 'ModifyDate', 'Name', 'Parent', 'Path', 'Size', 'Type')] 253 | [Alias('Criteria', 'Property')] 254 | [String] 255 | $SortingCriteria = 'ModifyDate', 256 | 257 | [Parameter(Position = 3)] 258 | [Alias('Desc')] 259 | [Switch] 260 | $Descending, 261 | 262 | [Parameter(Position = 4)] 263 | [ValidateScript({ $_ -gt 0 })] 264 | [Int16] 265 | $Top, 266 | 267 | [Parameter(Position = 1)] 268 | [Alias('Selector', 'Script', 'Lambda', 'Filter')] 269 | [ValidateNotNull()] 270 | [ScriptBlock] 271 | $SelectorScript 272 | ) 273 | 274 | process { 275 | $SelectedItems = @() + (New-Object -com shell.application).Namespace(10).Items() 276 | 277 | if ($OriginalPath) { 278 | $SelectedItems = $SelectedItems | Where-Object { $_.GetFolder.Title -eq $OriginalPath } 279 | } 280 | 281 | if ($OriginalPathRegex) { 282 | $SelectedItems = $SelectedItems | Where-Object { $_.GetFolder.Title -match $OriginalPathRegex } 283 | } 284 | 285 | if ($SelectorScript) { 286 | $SelectedItems = $SelectedItems | Where-Object { Invoke-Command $SelectorScript -ArgumentList $_ } 287 | } 288 | 289 | if ($SortingCriteria) { 290 | $SelectedItems = $SelectedItems | Sort-Object $SortingCriteria -Descending:$Descending 291 | } 292 | 293 | if ($Top) { 294 | $SelectedItems = $SelectedItems | Select-Object -First $Top 295 | } 296 | 297 | return $SelectedItems 298 | } 299 | <# 300 | .SYNOPSIS 301 | Get all items from the recycle bin, optionally filtered by the parameters 302 | .DESCRIPTION 303 | Get all items from the recycle bin, optionally filtered by the parameters 304 | .PARAMETER OriginalPath 305 | Filters recycle bin items by their original path 306 | .PARAMETER OriginalPathRegex 307 | Filters recycle bin items by their original path with a regex 308 | .PARAMETER SortingCriteria 309 | Sort output by the specified criteria 310 | .PARAMETER Descending 311 | Sort output descending instead of ascending 312 | .PARAMETER Top 313 | Only get top n results 314 | .PARAMETER SelectorScript 315 | Custom script to filter the results 316 | .INPUTS 317 | System.String The OriginalPath to search for 318 | .OUTPUTS 319 | System.__ComObject The recycle bin items 320 | .EXAMPLE 321 | Get-RecycledItems -OriginalPath "C:\Users\Kevin\Testfile" 322 | .EXAMPLE 323 | Get-RecycledItems -SortingCriteria "Size" -Descending -Top 5 324 | .EXAMPLE 325 | Get-RecycledItems -SelectorScript { $_.IsFolder -eq $true } 326 | .NOTES 327 | Author: Kevin Holtkamp, kevinholtkamp26@gmail.com 328 | LastEdit: 09.07.2022 329 | #> 330 | } 331 | 332 | Export-ModuleMember -Function Get-RecycledItem 333 | Export-ModuleMember -Function Remove-ItemSafely 334 | Export-ModuleMember -Function Restore-RecycledItem 335 | -------------------------------------------------------------------------------- /SslWebBinding/SslWebBinding.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'SslWebBinding' 3 | # 4 | # Generated by: Brian Dukes 5 | # 6 | # Generated on: 10/10/2016 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'SslWebBinding.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.4.0' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @('Desktop') 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'd8b5b233-6f01-4ade-b771-147cc9101072' 22 | 23 | # Author of this module 24 | Author = 'Brian Dukes' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Engage Software' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2022 Engage Software' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Adds (and trusts) and removes self-signed HTTPS bindings to websites in IIS' 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | # PowerShellVersion = '' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | RequiredModules = @( @{ ModuleName = 'AdministratorRole'; ModuleVersion = '1.0.1'; GUID = '694c2097-6b13-4735-8d6e-396224d646cc' }, 55 | @{ ModuleName = 'IISAdministration'; ModuleVersion = '1.1.0.0' } ) 56 | 57 | # Assemblies that must be loaded prior to importing this module 58 | # RequiredAssemblies = @() 59 | 60 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 61 | # ScriptsToProcess = @() 62 | 63 | # Type files (.ps1xml) to be loaded when importing this module 64 | # TypesToProcess = @() 65 | 66 | # Format files (.ps1xml) to be loaded when importing this module 67 | # FormatsToProcess = @() 68 | 69 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 70 | # NestedModules = @() 71 | 72 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 73 | FunctionsToExport = @('New-SslWebBinding', 'Remove-SslWebBinding') 74 | 75 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 76 | CmdletsToExport = @() 77 | 78 | # Variables to export from this module 79 | VariablesToExport = @() 80 | 81 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 82 | AliasesToExport = @() 83 | 84 | # DSC resources to export from this module 85 | # DscResourcesToExport = @() 86 | 87 | # List of all modules packaged with this module 88 | # ModuleList = @() 89 | 90 | # List of all files packaged with this module 91 | FileList = 'SslWebBinding.psm1' 92 | 93 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 94 | PrivateData = @{ 95 | 96 | PSData = @{ 97 | 98 | # Tags applied to this module. These help with module discovery in online galleries. 99 | Tags = @( 'PSEdition_Core', 'PSEdition_Desktop', 'Windows') 100 | 101 | # A URL to the license for this module. 102 | LicenseUri = 'https://github.com/bdukes/PowerShellModules/blob/main/LICENSE' 103 | 104 | # A URL to the main website for this project. 105 | ProjectUri = 'https://github.com/bdukes/PowerShellModules' 106 | 107 | # A URL to an icon representing this module. 108 | # IconUri = '' 109 | 110 | # ReleaseNotes of this module 111 | ReleaseNotes = 'https://github.com/bdukes/PowerShellModules/blob/main/CHANGES.md' 112 | 113 | ExternalModuleDependencies = @('PKI') 114 | 115 | 116 | } # End of PSData hashtable 117 | 118 | } # End of PrivateData hashtable 119 | 120 | # HelpInfo URI of this module 121 | # HelpInfoURI = '' 122 | 123 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 124 | # DefaultCommandPrefix = '' 125 | 126 | } 127 | 128 | -------------------------------------------------------------------------------- /SslWebBinding/SslWebBinding.psm1: -------------------------------------------------------------------------------- 1 | #Requires -Version 3 2 | #Requires -Modules IISAdministration, AdministratorRole, PKI 3 | Set-StrictMode -Version:Latest 4 | 5 | Import-Module IISAdministration 6 | 7 | function getHostHeader([string]$bindingInformation) { 8 | return $bindingInformation.Substring($bindingInformation.LastIndexOf(':') + 1); 9 | } 10 | function getPort([string]$bindingInformation) { 11 | $firstSeparatorIndex = $bindingInformation.IndexOf(':'); 12 | return $bindingInformation.Substring($firstSeparatorIndex + 1, $bindingInformation.LastIndexOf(':') - $firstSeparatorIndex - 1); 13 | } 14 | 15 | function New-SslWebBinding { 16 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Hardcoded default value, only used temporarily')] 17 | [CmdletBinding(SupportsShouldProcess)] 18 | param( 19 | [parameter(Mandatory = $true, position = 0)] 20 | [string]$siteName, 21 | [parameter(Mandatory = $false, position = 1)] 22 | [string[]]$hostHeader, 23 | [switch]$bypassMkcert 24 | ); 25 | 26 | Assert-AdministratorRole 27 | 28 | if (-not $hostHeader) { 29 | $hostHeader = @($siteName) 30 | } 31 | 32 | $hostHeader = $hostHeader | Select-Object -Unique 33 | 34 | $existingBindings = @($hostHeader | Foreach-Object { Get-IISSiteBinding -Name:$siteName -Protocol:https } | Where-Object { getHostHeader($_.BindingInformation) -eq $_ }); 35 | if ($existingBindings.Length -eq $hostHeader.Length) { 36 | foreach ($existingBinding in $existingBindings) { 37 | $domain = getHostHeader($existingBinding.bindingInformation); 38 | Write-Warning "Binding for https://$domain to $siteName already exists"; 39 | } 40 | 41 | return; 42 | } 43 | 44 | if ($existingBindings.Length -gt 0) { 45 | foreach ($existingBinding in $existingBindings) { 46 | $domain = getHostHeader($existingBinding.bindingInformation); 47 | if ($PSCmdlet.ShouldProcess($domain, 'Remove Binding')) { 48 | Remove-IISSiteBinding -Name:$siteName -BindingInformation:$existingBinding.bindingInformation -Protocol:https; 49 | } 50 | } 51 | } 52 | 53 | $certStoreLocation = 'Cert:\LocalMachine\My'; 54 | if (-not $bypassMkcert -and (Get-Command "mkcert" -ErrorAction SilentlyContinue)) { 55 | if ($PSCmdlet.ShouldProcess($siteName)) { 56 | $certRootDir = mkcert -CAROOT; 57 | $certFile = Join-Path -Path:$certRootDir -ChildPath:"$($siteName)_$(Get-Date -format 'yyyyMMddHHmmssfff').pfx"; 58 | mkcert -pkcs12 -p12-file $certFile $hostHeader; 59 | 60 | $defaultCertificatePassword = ConvertTo-SecureString 'changeit' -AsPlainText -Force; 61 | $cert = Import-PfxCertificate -FilePath:$certFile -Password:$defaultCertificatePassword -CertStoreLocation:$certStoreLocation; 62 | Remove-Item $certFile; 63 | 64 | foreach ($domain in $hostHeader) { 65 | Write-Information "Adding binding for https://$domain to $siteName"; 66 | New-IISSiteBinding -Name:$siteName -BindingInformation:"*:443:$domain" -Protocol:https -SslFlag:'Sni' -CertificateThumbPrint:$cert.Thumbprint -CertStoreLocation:$certStoreLocation; 67 | } 68 | } 69 | 70 | return; 71 | } 72 | 73 | foreach ($domain in $hostHeader) { 74 | if ($PSCmdlet.ShouldProcess($domain)) { 75 | Write-Information "Trusting generated SSL certificate for $hostHeader"; #based on https://stackoverflow.com/a/21001534 76 | $cert = New-SelfSignedCertificate -DnsName:$hostHeader -CertStoreLocation:$certStoreLocation 77 | $store = New-Object System.Security.Cryptography.X509Certificates.X509Store 'Root', 'CurrentUser'; 78 | $store.Open('ReadWrite'); 79 | $store.Add($cert); 80 | $store.Close(); 81 | 82 | Write-Information "Adding binding for https://$domain to $siteName"; 83 | New-IISSiteBinding -Name:$siteName -BindingInformation:"*:443:$domain" -Protocol:https -SslFlag:'Sni' -CertificateThumbPrint:$cert.Thumbprint -CertStoreLocation:$certStoreLocation; 84 | } 85 | } 86 | 87 | <# 88 | .SYNOPSIS 89 | Adds an HTTPS binding to a website in IIS 90 | .DESCRIPTION 91 | Generates a new self-signed SSL certificate and associates it with a new HTTPS binding in the given site, adding the certificate to the trusted certificate store on this machine. 92 | Uses mkcert to generate a single certificate for all host headers if available, otherwise generates a self-signed certificate per host header. 93 | .PARAMETER siteName 94 | The name of the site (the domain, folder name, and database name, e.g. dnn.local) 95 | .PARAMETER hostHeader 96 | The host header(s) for which to add the binding and certificate. Defaults to $siteName 97 | #> 98 | } 99 | 100 | function Remove-SslWebBinding { 101 | [CmdletBinding(SupportsShouldProcess)] 102 | param( 103 | [parameter(Mandatory = $true, position = 0)] 104 | [string]$siteName, 105 | [parameter(Mandatory = $false, position = 1)] 106 | [string[]]$hostHeader 107 | ); 108 | 109 | Assert-AdministratorRole 110 | 111 | if (-not $hostHeader) { 112 | $hostHeader = @($siteName) 113 | } 114 | 115 | foreach ($existingBinding in @($hostHeader | Foreach-Object { Get-IISSiteBinding -Name:$siteName -Protocol:https } | Where-Object { getHostHeader($_.BindingInformation) -eq $_ })) { 116 | $certHash = $existingBinding['certificateHash']; 117 | $cert = Get-Item Cert:\LocalMachine\My\$certHash -ErrorAction SilentlyContinue; 118 | if ($cert -and $PSCmdlet.ShouldProcess($cert.Subject, 'Remove Certificate')) { 119 | Remove-Item Cert:\LocalMachine\My\$certHash; 120 | } 121 | if ($PSCmdlet.ShouldProcess($existingBinding.BindingInformation, 'Remove Binding')) { 122 | Remove-IISSiteBinding -Name:$siteName -BindingInformation:$existingBinding.BindingInformation -Protocol:https -ErrorAction:Continue -Confirm:$false; 123 | } 124 | } 125 | 126 | <# 127 | .SYNOPSIS 128 | Removes an HTTPS binding to a website in IIS 129 | .DESCRIPTION 130 | Removes an HTTPS binding in the given site 131 | .PARAMETER siteName 132 | The name of the site (the domain, folder name, and database name, e.g. dnn.local) 133 | .PARAMETER hostHeader 134 | The host header(s) for which to remove the binding. Defaults to $siteName 135 | #> 136 | } 137 | 138 | Export-ModuleMember New-SslWebBinding 139 | Export-ModuleMember Remove-SslWebBinding 140 | -------------------------------------------------------------------------------- /Write-HtmlNode/Write-HtmlNode.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'Write-HtmlNode' 3 | # 4 | # Generated by: Brian Dukes 5 | # 6 | # Generated on: 10/10/2016 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'Write-HtmlNode.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '2.0.1' 16 | 17 | # Supported PSEditions 18 | CompatiblePSEditions = @('Desktop') 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '941aad91-17a6-43a5-bb1c-cce8526d7b3e' 22 | 23 | # Author of this module 24 | Author = 'Brian Dukes' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Engage Software' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2021 Engage Software' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Writes the given HTML node with color.' 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | PowerShellVersion = '5.1' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = 'Write-HtmlNode' 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = @() 76 | 77 | # Variables to export from this module 78 | VariablesToExport = @() 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = @() 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | FileList = 'Write-HtmlNode.psm1' 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | Tags = @('PSEdition_Desktop', 'Windows') 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'https://github.com/bdukes/PowerShellModules/blob/main/LICENSE' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/bdukes/PowerShellModules' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | ReleaseNotes = 'https://github.com/bdukes/PowerShellModules/blob/main/CHANGES.md' 111 | 112 | } # End of PSData hashtable 113 | 114 | } # End of PrivateData hashtable 115 | 116 | # HelpInfo URI of this module 117 | # HelpInfoURI = '' 118 | 119 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 120 | # DefaultCommandPrefix = '' 121 | 122 | } 123 | 124 | -------------------------------------------------------------------------------- /Write-HtmlNode/Write-HtmlNode.psm1: -------------------------------------------------------------------------------- 1 | Set-StrictMode -Version Latest 2 | 3 | function Write-HtmlNode($node, $indent = '', [switch]$excludeAttributes, [switch]$excludeEmptyElements, [switch]$excludeComments) { 4 | if ($excludeEmptyElements -and $node.nodeName -ne '#text' -and $node.nodeName -ne '#comment' -and $node.canHaveChildren -eq $false) { 5 | return 6 | } 7 | if ($excludeComments -and $node.nodeName -eq '#comment') { 8 | return 9 | } 10 | 11 | Write-Host $indent 12 | if ($node.nodeName -eq '#text') { 13 | Write-Host $node.nodeValue -ForegroundColor White 14 | return 15 | } 16 | elseif ($node.nodeName -eq '#comment') { 17 | Write-Host $node.OuterHtml -ForegroundColor DarkGreen 18 | return 19 | } 20 | Write-Host '<' -NoNewline -ForegroundColor Gray 21 | Write-Host $node.nodeName -NoNewline -ForegroundColor Blue 22 | if ($excludeAttributes -eq $false) { 23 | foreach ($attr in ($node.attributes | Where-Object { $_.Specified })) { 24 | Write-Host ' ' -NoNewline 25 | Write-Host $attr.name -NoNewline -ForegroundColor Magenta 26 | Write-Host '="' -NoNewline -ForegroundColor Gray 27 | Write-Host $attr.value -NoNewline -ForegroundColor Yellow 28 | Write-Host '"' -NoNewline -ForegroundColor Gray 29 | } 30 | } 31 | if ($node.canHaveChildren -eq $false) { 32 | Write-Host ' />' -ForegroundColor Gray 33 | return 34 | } 35 | Write-Host '>' -ForegroundColor Gray 36 | $child = $node.firstChild 37 | $childIndent = $indent + ' ' 38 | while ($null -ne $child) { 39 | write-htmlNode $child $childIndent -excludeAttributes:$excludeAttributes -excludeEmptyElements:$excludeEmptyElements -excludeComments:$excludeComments 40 | $child = $child.nextSibling 41 | } 42 | Write-Host $indent -NoNewline 43 | Write-Host '' -ForegroundColor Gray 46 | <# 47 | .SYNOPSIS 48 | Writes the given HTML node with color 49 | .PARAMETER node 50 | An HTML node, probably from (Invoke-WebRequest $url).ParsedHtml.documentElement 51 | .PARAMETER indent 52 | How much of an indent to add before the first node 53 | .PARAMETER excludeAttributes 54 | Whether to display attributes of the elements 55 | .PARAMETER excludeEmptyElements 56 | Whether to display elements that cannot have any content 57 | .PARAMETER excludeComments 58 | Whether to display the HTML comments 59 | #> 60 | } 61 | 62 | Export-ModuleMember Write-HtmlNode -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - main 3 | 4 | pool: 5 | vmImage: "vs2017-win2016" 6 | 7 | steps: 8 | - powershell: "Install-Module Pester -Scope CurrentUser -Force" 9 | displayName: "Install Pester" 10 | 11 | - powershell: "Invoke-Pester -OutputFile $(System.DefaultWorkingDirectory)/Test-Pester.XML -OutputFormat NUnitXML" 12 | displayName: "Run tests" 13 | 14 | - task: PublishTestResults@2 15 | inputs: 16 | testResultsFormat: "NUnit" 17 | testResultsFiles: "Test-Pester.XML" 18 | searchFolder: "$(System.DefaultWorkingDirectory)" 19 | failTaskOnFailedTests: true 20 | displayName: "Publish test results" 21 | --------------------------------------------------------------------------------