core/manager/projects/projects-main.ps1

<#
    .SYNOPSIS
    Provisions a new sitefinity instance project.
    .DESCRIPTION
    Gets latest from the branch, builds and starts a sitefinity instance with default admin user username:admin pass:admin@2. The local path where the project files are created is specified in the constants script file (EnvConstants.ps1).
    .PARAMETER name
    The name of the new sitefinity instance.
    .PARAMETER branch
    The tfs branch from which the Sitefinity source code is downloaded. It has predefined values that can be iterated by pressing tab repeatedly.
    .PARAMETER buildSolution
    Builds the solution after downloading from tfs.
    .PARAMETER startWebApp
    Starts webapp after building the solution.
    .OUTPUTS
    None
#>

function sf-proj-new {
    Param(
        [Parameter(Mandatory = $true)][string]$sourcePath,
        [string]$displayName = 'Untitled'
    )

    if (!$sourcePath) {
        $sourcePath = _sf-proj-promptSourcePathSelect
    }

    [SfProject]$newContext = _newSfProjectObject
    $newContext.displayName = $displayName

    _createProjectFilesFromSource -sourcePath $sourcePath -project $newContext
    if (!$newContext.webAppPath) {
        throw "Error creating the project. The project failed to initialize with web app path."
    }

    _sf-proj-tags-setNewProjectDefaultTags -project $newContext
    _saveSelectedProject $newContext
    sf-proj-setCurrent $newContext        

    if (!$newContext.websiteName) {
        sf-iis-site-new
    }

    _createUserFriendlySlnName $newContext
    return $newContext
}

function sf-proj-clone {
    Param(
        [switch]$skipSourceControlMapping
    )

    $context = sf-proj-getCurrent

    $sourcePath = $context.solutionPath;
    $hasSolution = !([string]::IsNullOrEmpty($sourcePath));
    if (!$hasSolution) {
        $sourcePath = $context.webAppPath
    }

    if (!$sourcePath -or !(Test-Path $sourcePath)) {
        throw "Invalid app path";
    }

    $targetDirectoryName = [Guid]::NewGuid()
    $targetPath = $GLOBAL:Sf.Config.projectsDirectory + "\$targetDirectoryName"
    if (Test-Path $targetPath) {
        throw "Path exists: ${targetPath}"
    }

    try {
        Write-Information "Copying $sourcePath to $targetPath."
        New-Item $targetPath -ItemType Directory > $null
        Copy-Item "${sourcePath}\*" $targetPath -Recurse
    }
    catch {
        $errors = "Error copying source files.`n $_";
        try {
            Remove-Item $targetPath -Force -Recurse -ErrorVariable +errors -ErrorAction SilentlyContinue
        }
        finally {
            throw $errors
        }
    }

    [SfProject]$newProject = $null
    [SfProject]$newProject = _newSfProjectObject
    $newProject.displayName = "$($context.displayName)-clone"
    if ($hasSolution) {
        $newProject.solutionPath = $targetPath
        $newProject.webAppPath = "$targetPath\SitefinityWebApp"
        _createUserFriendlySlnName -context $newProject
    }
    else {
        $newProject.webAppPath = $targetPath
    }

    sf-proj-setCurrent -newContext $newProject

    try {
        if (!$skipSourceControlMapping -and $context.branch) {
            _createWorkspace -context $newProject -branch $context.branch
        }
    }
    catch {
        Write-Error "Clone project error. Error binding to TFS.`n$_"
    }

    try {
        Write-Information "Creating website..."
        sf-iis-site-new
    }
    catch {
        Write-Warning "Error during website creation. Message: $_"
        $newProject.websiteName = ""
    }

    $oldProject = $context
    $sourceDbName = _sf-app-db-getName $oldProject.webAppPath
    $isDuplicate = sql-test-isDbNameDuplicate -dbName $sourceDbName
    if ($sourceDbName -and $isDuplicate) {
        $newDbName = $newProject.id
        try {
            sf-app-db-setName $newDbName -context $newProject
        }
        catch {
            Write-Error "Error setting new database name in config $newDbName).`n $_"                    
        }
                
        try {
            sql-copy-db -SourceDBName $sourceDbName -targetDbName $newDbName
        }
        catch {
            Write-Error "Error copying old database. Source: $sourceDbName Target $newDbName`n $_"
        }
    }

    try {
        sf-app-states-removeAll
    }
    catch {
        Write-Error "Error deleting app states for $($newProject.displayName). Inner error:`n $_"        
    }
}

<#
    .SYNOPSIS
    Imports a new sitefinity instance project from given local path.
    .DESCRIPTION
    A sitefinity web app project or Sitefinity solution can be imported.
    .PARAMETER displyName
    The name of the imported sitefinity instance.
    .PARAMETER path
    The directory which contains either Telerik.Sitefinity.sln or SitefinityWebApp.csproj files. The app automatically detects whether the full Sitefinity source code or just the webapp that uses Sitefinity CMS is available.
    .OUTPUTS
    None
#>

function _sf-proj-tryUseExisting {
    
    Param(
        [Parameter(Mandatory = $true)][SfProject]$project,
        [Parameter(Mandatory = $true)][string]$path
    )

    
    if (Test-Path -Path "$path\SitefinityWebApp") {
        $path = "$path\SitefinityWebApp"
    }

    $isWebApp = Test-Path "$path\web.config"
    if (!$isWebApp) {
        return
    }

    $project.webAppPath = $path
    sf-proj-setCurrent $project
    _sf-proj-refreshData -project $project
    _saveSelectedProject $project
    return $true
}

function sf-proj-removeBulk {
    $sitefinities = @(sf-data-getAllProjects)
    if ($null -eq $sitefinities[0]) {
        Write-Host "No projects found. Create one."
        return
    }

    $sfsToDelete = _sf-proj-promptSfsSelection $sitefinities

    foreach ($selectedSitefinity in $sfsToDelete) {
        try {
            sf-proj-remove -context $selectedSitefinity -noPrompt
        }
        catch {
            Write-Error "Error deleting project with id = $($selectedSitefinity.id)"       
        }
    }
}

<#
    .SYNOPSIS
    Deletes a sitefinity instance managed by the script.
    .DESCRIPTION
    Everything is deleted - local project files, database, TFS workspace if no switches are passed.
    .PARAMETER keepWorkspace
    Keeps the workspace if one exists.
    .PARAMETER keepProjectFiles
    Keeps the project files.
    .PARAMETER keepProjectFiles
    Forces the deletion by resetting IIS to free any locked files by the app.
    .OUTPUTS
    None
#>

function sf-proj-remove {
    Param(
        [switch]$keepDb,
        [switch]$keepWorkspace,
        [switch]$keepProjectFiles,
        [switch]$noPrompt,
        [SfProject]$context = $null
    )
    
    [SfProject]$currentProject = sf-proj-getCurrent
    $clearCurrentSelectedProject = $false
    if ($null -eq $context -or $currentProject.id -eq $context.id) {
        $context = $currentProject
        $clearCurrentSelectedProject = $true
    }
    
    # Del Website
    Write-Information "Deleting website..."
    $websiteName = $context.websiteName
    if ($websiteName -and (iis-test-isSiteNameDuplicate $websiteName)) {
        try {
            sf-iis-pool-stop $websiteName
        }
        catch {
            Write-Warning "Could not stop app pool: $_`n"            
        }

        try {
            _sf-iis-site-delete $context.websiteName
        }
        catch {
            Write-Warning "Errors deleting website ${websiteName}. $_`n"
        }
    }

    # TFS
    $workspaceName = $null
    try {
        Set-Location -Path $PSScriptRoot
        $workspaceName = tfs-get-workspaceName $context.webAppPath
    }
    catch {
        Write-Warning "No workspace to delete, no TFS mapping found."        
    }
    
    if ($workspaceName -and !($keepWorkspace)) {
        Write-Information "Deleting workspace..."
        try {
            tfs-delete-workspace $workspaceName $GLOBAL:Sf.Config.tfsServerName
        }
        catch {
            Write-Warning "Could not delete workspace $_"
        }
    }

    $dbName = _sf-app-db-getName -appPath $context.webAppPath

    # Del db
    if (-not [string]::IsNullOrEmpty($dbName) -and (-not $keepDb)) {
        Write-Information "Deleting sitefinity database..."
        
        try {
            sql-delete-database -dbName $dbName
        }
        catch {
            Write-Warning "Could not delete database: ${dbName}. $_"
        }
    }

    # Del dir
    if (!($keepProjectFiles)) {
        try {
            $solutionPath = $context.solutionPath
            if ($solutionPath) {
                $path = $solutionPath
            }
            else {
                $path = $context.webAppPath
            }
            
            Write-Information "Unlocking all locked files in solution directory..."
            unlock-allFiles -path $path

            Write-Information "Deleting solution directory..."
            Remove-Item $path -recurse -force -ErrorAction SilentlyContinue -ErrorVariable ProcessError
            if ($ProcessError) {
                throw $ProcessError
            }
        }
        catch {
            Write-Warning "Errors deleting sitefinity directory. $_"
        }
    }

    Write-Information "Deleting data entry..."
    try {
        _removeProjectData $context
    }
    catch {
        Write-Warning "Could not remove the project entry from the tool. You can manually remove it at $($GLOBAL:Sf.Config.dataPath)"
    }
    
    if ($clearCurrentSelectedProject) {
        sf-proj-setCurrent $null
    }

    if (-not ($noPrompt)) {
        sf-proj-select
    }
}

function sf-proj-rename {
    Param(
        [string]$newName,
        [switch]$setDescription
    )

    $project = sf-proj-getCurrent
    [SfProject]$context = $project

    if (-not $newName) {
        while ([string]::IsNullOrEmpty($newName)) {
            if ($newName) {
                Write-Warning "Invalid name syntax."
            }

            $newName = $(Read-Host -Prompt "Enter new project name").ToString()
        }

        if ($setDescription) {
            $context.description = $(Read-Host -Prompt "Enter description:`n").ToString()
        }
    }

    $azureDevOpsResult = _getAzureDevOpsTitleAndLink $newName
    $newName = $azureDevOpsResult.name
    $context.description = $azureDevOpsResult.link

    if ($newName -and (-not (_validateNameSyntax $newName))) {
        Write-Error "Name syntax is not valid. Use only alphanumerics and underscores"
    }

    $oldSolutionName = _generateSolutionFriendlyName -context $context
    $context.displayName = $newName

    if ($context.solutionPath) {
        if (-not (Test-Path "$($context.solutionPath)\$oldSolutionName")) {
            _createUserFriendlySlnName -context $context
        }
    
        $newSolutionName = _generateSolutionFriendlyName -context $context
        $oldSolutionPath = "$($context.solutionPath)\$oldSolutionName"
        if (Test-Path $oldSolutionPath) {
            Copy-Item -Path $oldSolutionPath -Destination "$($context.solutionPath)\$newSolutionName" -Force
        
            $newSlnCacheName = ([string]$newSolutionName).Replace(".sln", "")
            $oldSlnCacheName = ([string]$oldSolutionName).Replace(".sln", "")
            $oldSolutionCachePath = "$($context.solutionPath)\.vs\$oldSlnCacheName"
            if (Test-Path $oldSolutionCachePath) {
                Copy-Item -Path $oldSolutionCachePath -Destination "$($context.solutionPath)\.vs\$newSlnCacheName" -Force -Recurse -ErrorAction SilentlyContinue
                unlock-allFiles -path $oldSolutionCachePath
                Remove-Item -Path $oldSolutionCachePath -Force -Recurse
            }
        
            unlock-allFiles -path $oldSolutionPath
            Remove-Item -Path $oldSolutionPath -Force
        }
    }
    
    $domain = _generateDomainName -context $context
    _changeDomain -domainName $domain
    Set-Prompt -project $context
    
    _saveSelectedProject $context
}

<#
.SYNOPSIS
Undos all pending changes, gets latest, builds and initializes.
#>

function sf-proj-reset {
    $shouldReset = $false
    if (sf-tfs-hasPendingChanges) {
        sf-tfs-undoPendingChanges
        $shouldReset = $true
    }

    $getLatestOutput = sf-tfs-getLatestChanges -overwrite
    if (-not ($getLatestOutput.Contains('All files are up to date.'))) {
        $shouldReset = $true
    }

    if ($shouldReset) {
        sf-sol-clean -cleanPackages $true
        sf-app-reset -start -rebuild -precompile
        sf-app-states-save -stateName initial
    }
}

function sf-proj-getCurrent {
    $currentContext = $Script:globalContext

    if ($null -eq $currentContext) {
        return $null
    }

    $context = $currentContext.PsObject.Copy()
    return [SfProject]$context
}

function sf-proj-setCurrent {
    Param(
        [SfProject]$newContext
    )
        
    if ($null -ne $newContext) {
        _validateProject $newContext        
    } 

    $Script:globalContext = $newContext
    Set-Prompt -project $newContext
}

function _getAzureDevOpsTitleAndLink {
    Param([string]$name)
    $description = ''
    $titleKeys = @("Product Backlog Item ", "Bug ", "Task ");
    foreach ($key in $titleKeys) {
        if ($name.StartsWith($key)) {
            $name = $name.Replace($key, '');
            $nameParts = $name.Split(':');
            $itemId = $nameParts[0].Trim();
            $title = $nameParts[1].Trim();
            $resultTitle = ''
            for ($i = 0; $i -lt $name.Length; $i++) {
                $resultTitle = "${resultTitle}:$($title[$i])";
            }
            
            $name = $name.Trim();
            $name = _getValidTitle $name

            $description = "https://prgs-sitefinity.visualstudio.com/sitefinity/_workitems/edit/$itemId"
        }
    }

    return @{ name = $name; link = $description }
}

function _getValidTitle {
    param (
        [string]$title
    )

    $validStartEnd = "^[A-Za-z]$";
    $validMiddle = "^\w$";
    while (!($title[0] -match $validStartEnd)) {
        $title = $title.Substring(1)
    }

    while (!($title[$title.Length - 1] -match $validStartEnd)) {
        $title = $title.Substring($title.Length - 2)
    }

    $resultTitle = '';
    for ($i = 0; $i -lt $title.Length; $i++) {
        if ($title[$i] -match $validMiddle) {
            $resultTitle = "$resultTitle$($title[$i])"
        }
        elseif ($title[$i] -eq ' ') {
            $resultTitle = "${resultTitle}_"
        }
    }

    if ($resultTitle.Length -ge 51) {
        $resultTitle = $resultTitle.Remove(50);
    }
    
    return $resultTitle;
}

function _createUserFriendlySlnName ($context) {
    $solutionFilePath = "$($context.solutionPath)\Telerik.Sitefinity.sln"
    if (!(Test-Path $solutionFilePath)) {
        Write-Warning "Solution file not available."
        return
    }

    $targetFilePath = "$($context.solutionPath)\$(_generateSolutionFriendlyName $context)"
    if (!(Test-Path $targetFilePath)) {
        Copy-Item -Path $solutionFilePath -Destination $targetFilePath
    }
}

function _saveSelectedProject {
    Param($context)

    _validateProject $context

    _setProjectData $context
}

function _validateProject {
    Param($context)

    if (!$context.id) {
        throw "Invalid sitefinity context. No sitefinity id."
    }

    if ($context.solutionPaths) {
        if (-not (Test-Path $context.solutionPath)) {
            throw "Invalid sitefinity context. Solution path does not exist."
        }
    }
        
    if (-not ($context.webAppPath -and (Test-Path $context.webAppPath))) {
        throw "Invalid sitefinity context. No web app path or it does not exist."
    }
}

function _getIsIdDuplicate ($id) {
    function _isDuplicate ($name) {
        if ($name -and $name.Contains($id)) {
            return $true
        }
        return $false
    }

    $sitefinities = [SfProject[]](sf-data-getAllProjects)
    $sitefinities | ForEach-Object {
        $sitefinity = [SfProject]$_
        if ($sitefinity.id -eq $id) {
            return $true;
        }
    }

    if (Test-Path "$($GLOBAL:Sf.Config.projectsDirectory)\$id") { return $true }

    $wss = tfs-get-workspaces $GLOBAL:Sf.Config.tfsServerName | Where-Object { _isDuplicate $_ }
    if ($wss) { return $true }

    Import-Module WebAdministration
    $sites = Get-Item "IIS:\Sites"
    if ($sites -and $sites.Children) {
        $names = $sites.Children.Keys | Where-Object { _isDuplicate $_ }
        if ($names) { return $true }
    }
    $pools = Get-Item "IIS:\AppPools"
    if ($pools -and $pools.Children) {
        $names = $pools.Children.Keys | Where-Object { _isDuplicate $_ }
        if ($names) { return $true }
    }
    
    $dbs = sql-get-dbs | Where-Object { _isDuplicate $_.name }
    if ($dbs) { return $true }

    return $false;
}

function _generateId {
    $i = 0;
    while ($true) {
        $name = "$($GLOBAL:Sf.Config.idPrefix)$i"
        $_isDuplicate = (_getIsIdDuplicate $name)
        if (-not $_isDuplicate) {
            break;
        }
        
        $i++
    }

    if ([string]::IsNullOrEmpty($name) -or (-not (_validateNameSyntax $name))) {
        throw "Invalid id $name"
    }
    
    return $name
}

function _generateSolutionFriendlyName {
    Param(
        [SfProject]$context
    )
    
    if (-not ($context)) {
        $context = sf-proj-getCurrent
    }

    $solutionName = "$($context.displayName)($($context.id)).sln"
    
    return $solutionName
}

function _validateNameSyntax ($name) {
    return $name -match "^[A-Za-z]\w+$" -and $name.Length -lt 75
}

function _sf-proj-refreshData {
    param (
        [Parameter(Mandatory = $true)][SfProject]$project
    )
    
    if ($project.isInitialized) {
        return
    }

    $errorMessgePrefix = "ERROR Working with project $($project.displayName) in $($project.webAppPath) and id $($project.id)."

    if (!(Test-Path $project.webAppPath)) {
        if (!$suppressWarnings) {
            throw "$errorMessgePrefix $($project.webAppPath) does not exist."
        }
        else {
            return
        }
    }

    _sf-proj-detectSolution -project $project
    _sf-proj-detectTfs -project $project
    _sf-proj-detectSite -project $project

    _saveSelectedProject -context $project
    $project.isInitialized = $true
}

function _createProjectFilesFromSource {
    param (
        [Parameter(Mandatory = $true)][Sfproject]$project,
        [Parameter(Mandatory = $true)][string]$sourcePath
    )
    
    $handled = _sf-proj-tryCreateFromBranch -project $project -sourcePath $sourcePath
    
    if (!$handled) {
        $handled = _sf-proj-tryCreateFromZip -project $project -sourcePath $sourcePath
    }

    if (!$handled) {
        $handled = _sf-proj-tryUseExisting -project $project -path $sourcePath
    }

    if (!$handled) {
        throw "Source path does not exist"
    }
}

function _sf-proj-createProjectDirectory {
    param (
        [Parameter(Mandatory = $true)][Sfproject]$project
    )
    
    Write-Information "Creating project files..."
    $projectDirectory = "$($GLOBAL:Sf.Config.projectsDirectory)\$($project.id)"
    if (Test-Path $projectDirectory) {
        throw "Path already exists:" + $projectDirectory
    }

    New-Item $projectDirectory -type directory > $null
    $projectDirectory
}

function _sf-proj-detectSolution ([SfProject]$project) {
    if (_sf-proj-isSolution -project $project) {
        $project.solutionPath = (Get-Item "$($project.webAppPath)\..\").Target
        _createUserFriendlySlnName $project
    }
    else {
        $project.solutionPath = ''
    }

    _saveSelectedProject -context $project
}

function _sf-proj-tryCreateFromBranch {
    param (
        [Parameter(Mandatory = $true)][Sfproject]$project,
        [Parameter(Mandatory = $true)][string]$sourcePath
    )

    if ($sourcePath.StartsWith("$/CMS/")) {
        $projectDirectory = _sf-proj-createProjectDirectory -project $project
        Write-Information "Creating project files..."
        $project.solutionPath = $projectDirectory;
        $project.webAppPath = "$projectDirectory\SitefinityWebApp";
        _createWorkspace -context $project -branch $sourcePath
        return $true
    }
}

function _sf-proj-tryCreateFromZip {
    param (
        [Parameter(Mandatory = $true)][Sfproject]$project,
        [Parameter(Mandatory = $true)][string]$sourcePath
    )

    if ($sourcePath.EndsWith('.zip') -and (Test-Path $sourcePath)) {
        $projectDirectory = _sf-proj-createProjectDirectory -project $project
        expand-archive -path $sourcePath -destinationpath $projectDirectory
        $isSolution = (Test-Path -Path "$projectDirectory/Telerik.Sitefinity.sln") -and (Test-Path "$projectDirectory/SitefinityWebApp")
        if ($isSolution) {
            $project.webAppPath = "$projectDirectory/SitefinityWebApp"
            $project.solutionPath = "$projectDirectory"
        }
        else {
            $project.webAppPath = "$projectDirectory"
        }

        return $true
    }
}

function _sf-proj-detectTfs ([SfProject]$project) {
    if (!(_sf-proj-isSolution -project $project)) {
        $project.branch = '';
        _saveSelectedProject -context $project
        return
    }

    $branch = tfs-get-branchPath -path $project.solutionPath
    if ($branch) {
        $project.branch = $branch
        _updateLastGetLatest -context $project
    }
    else {
        $project.branch = '';
        Write-Warning "Could not detect source control branch"
    }

    _saveSelectedProject -context $project
}

function _sf-proj-isSolution ([SfProject]$project) {
    Test-Path "$($project.webAppPath)\..\Telerik.Sitefinity.sln"
}

function _sf-proj-detectSite ([Sfproject]$project) {
    $siteName = iis-find-site -physicalPath $project.webAppPath
    if ($siteName) {
        $project.websiteName = $siteName
    }
    else {
        $project.websiteName = ''
        Write-Warning "$errorMessgePrefix Could not detect website for the current project."
    }
}