PSBrowserBookmarks.psm1

#region classes
class BrowserBookmark {
    [string]$Source
    [string]$URL
    [string]$Title
    [string]$Path
    [string]$BMKGuid
    [string]$URLGuid
    [string]$URLHash
    [int]$Order
    [Nullable[datetime]]$DateAdded
    [string]$Description
    [string]$IconFile
    [int]$IconIndex
    [Nullable[int]]$Stars
    [string]$Notes
    BrowserBookmark([string]$Source) {
        $this.Source = $Source
        $this.DateAdded = $null
        $this.IconIndex = -1
        $this.Order = 0
    }
}
class BookmarkOperationResult {
    [int]$BookmarksAdded = 0
    [int]$BookmarksSkipped = 0
    [int]$BookmarksRemoved = 0
    [int]$ErrorsEncountered = 0
    [string]$TargetBrowser
    [string]$TargetPath = ''
    [string]$BackupFile = ''
    BookmarkOperationResult([string]$TargetBrowser) {
        $this.TargetBrowser = $TargetBrowser
    }
    BookmarkOperationResult([string]$TargetBrowser,[string]$TargetPath) { 
        $this.TargetBrowser = $TargetBrowser
        $this.TargetPath = $TargetPath
    }
}

# this is strictly a helper class
class ChromeBookmarkItem {
    [string]$guid
    [string]$date_added
    [string]$date_modified
    [int]$id
    [string]$name
    [string]$type = 'url'
    [string]$url
    ChromeBookmarkItem([int]$id,[string]$name,[string]$url) {
        $this.id = $id
        $this.name = $name
        $this.url = $url
        $this.guid = [GUID]::NewGuid().GUID
    }
}

# this is strictly a helper class
class ChromeBookmarkFolder {
    [Object[]]$children = @()
    [string]$guid
    [string]$date_added
    [string]$date_modified
    [int]$id
    [string]$name
    [string]$type = 'folder'
    ChromeBookmarkFolder([int]$id,[string]$name) {
        $this.id = $id
        $this.name = $name
        $this.guid = [GUID]::NewGuid().GUID
    }
}
#endregion

#region base functions
function Get-IniContent {
    Param(
        [Parameter()][string]$FilePath
    )
    <#
        This is a helper function to read the Firefox settings.ini and IE .url files into custom object
        Stolen from https://devblogs.microsoft.com/scripting/use-powershell-to-work-with-any-ini-file/
    #>

    $ini = $null
    if (Test-Path $FilePath) {
        $ini = @{}
        switch -regex -file $FilePath  
        {  
            "^\[(.+)\]$" # Section
            {  
                $section = $matches[1]  
                $ini[$section] = @{}  
                $CommentCount = 0  
            }  
            "^(;.*)$" # Comment
            { }   
            "(.+?)\s*=\s*(.*)" # Key
            { 
                if (!($section))  
                {  
                    $section = "No-Section"  
                    $ini[$section] = @{}  
                }  
                $name,$value = $matches[1..2]  
                $ini[$section][$name] = $value  
            }  
        }  
    }
    return $ini
}
#endregion

#region generic

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function New-BrowserBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)][ValidateScript({if ($_ -as [uri]) { $true }})][string]$URL,
        [Parameter(Mandatory=$true)][string]$Title,
        [Parameter(Mandatory=$false)][string]$Description,
        [Parameter(Mandatory=$false)][string]$Path,
        [Parameter(Mandatory=$false)][ValidateRange(0,5)][Nullable[int]]$Stars,
        [Parameter(Mandatory=$false)][string]$Notes,
        [Parameter(Mandatory=$false)][Nullable[datetime]]$DateAdded = (Get-Date),
        [Parameter(Mandatory=$false)][switch]$AttemptIconDiscovery
    )
    $bmk_obj = New-Object BrowserBookmark ('generic')
    $bmk_obj.URL = $URL
    $bmk_obj.Title = $Title
    $bmk_obj.DateAdded = $DateAdded
    $bmk_obj.Path = $Path
    if ($null -ne $Stars) {
        $bmk_obj.Stars = $Stars
    }
    if ($Notes) {
        $bmk_obj.Notes = $Notes
    }
    if ($AttemptIconDiscovery) {
        try {
            $html = Invoke-WebRequest $URL
        } catch {
            Write-Warning ($moduleMessages.nbb002 -f $_.Exception.Message)
            $html = $null
        }
        if ($html) {
            $icon_rels = @('shortcut icon','apple-touch-icon','icon')
            $icon_tag = $html.ParsedHtml.getElementsByTagName('link') | where {$_.rel -in $icon_rels} | Select -First 1
            if ($icon_tag) {
                Write-Verbose ($moduleMessages.nbb004 -f $icon_tag.href,$icon_tag.rel)
                if ($icon_tag.href -like "//*") {
                    # root relative
                    $bmk_obj.IconFile = "$($URL.Substring(0,$URL.IndexOf("://"))):$($icon_tag.href)"
                } elseif ($icon_tag.href -like "/*") {
                    # relative
                    if ($URL.LastIndexOf('/') -eq ($URL.IndexOf('://') + 2)) {
                        $bmk_obj.IconFile = "$URL$($icon_tag.href)"
                    } else {
                        $bmk_obj.IconFile = "$($URL.Substring(0,$URL.LastIndexOf('/')))$($icon_tag.href)"
                    }
                } elseif ($icon_tag.href -match "^http(s*):\/\/") {
                    # absolute
                    $bmk_obj.IconFile = $icon_tag.href
                } else {
                    # invalid
                    Write-Verbose $moduleMessages.nbb005
                }
                if ($bmk_obj.IconFile) {
                    Write-Verbose ($moduleMessages.nbb006 -f $bmk_obj.IconFile)
                    #2do download file and calculate icon index
                    $bmk_obj.IconIndex = 1
                }
            } else {
                Write-Verbose ($moduleMessages.nbb003 -f ($icon_rels -join ','))
            }
        }
    }
    Write-Verbose $moduleMessages.nbb001
    $bmk_obj
}
#endregion

#region firefox

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Get-FirefoxBookmarkLocation {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named File')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$PlacesFile
    )
    if ($script:SQLiteLibPath) {
        try {
            Add-Type -Path $script:SQLiteLibPath -EA Stop
        } catch {
            Write-Warning ($moduleMessages.init02 -f $_.Exception.Message)
            return
        }
    } else {
        return
    }
    $res = $null
    $path = $null
    if ($DefaultProfile) {
        $moz_defaultprofile = $null
        $moz_basepath = "$($env:APPDATA)\Mozilla\Firefox"
        Write-Verbose ($moduleMessages.gfl001 -f $moz_basepath)
        if (Test-Path "$moz_basepath\installs.ini" -PathType Leaf) {
            $ini_file = Get-IniContent "$moz_basepath\installs.ini"
            Write-Verbose ($moduleMessages.gfl002 -f $ini_file.Count)
            if ($ini_file.Count -gt 0) {
                $default_profile = $ini_file[($ini_file.GetEnumerator()[0]).Name]["Default"] -replace "\/","\"
                $moz_defaultprofile = "$moz_basepath\$default_profile"
                Write-Verbose ($moduleMessages.gfl003 -f $moz_defaultprofile)
                if ($moz_defaultprofile) {
                    $moz_placespath = "$moz_defaultprofile\places.sqlite"
                    if (Test-Path $moz_placespath) {
                        $path = $moz_placespath
                    } else {
                        Write-Verbose ($moduleMessages.gfl005 -f $moz_placespath)
                    }
                }
            }
        } else {
            Write-Verbose ($moduleMessages.gfl004 -f $moz_basepath)
        }
    } else {
        if (Test-Path $PlacesFile) {
            $path = $PlacesFile
        } else {
            Write-Verbose ($moduleMessages.gfl005 -f $PlacesFile)
        }
    }
    if ($path) {
        $sql_ok = $false
        $dbc_sql = New-Object -TypeName System.Data.SQLite.SQLiteConnection
        $dbc_sql.ConnectionString = "Data Source=$path"
        try {
            $dbc_sql.Open()
        } catch {
            Write-Warning ($moduleMessages.sql001 -f $_.Exception.Message)
        }
        if ($dbc_sql.State -eq "Open") {
            $cmd_sql = $dbc_sql.CreateCommand()
            $cmd_sql.CommandText = "SELECT COUNT(*) FROM moz_bookmarks WHERE type <> 1"
            $nbm = $cmd_sql.ExecuteScalar()
            $cmd_sql.Dispose()
            if ($nbm -gt 0) {
                Write-Verbose ($ModuleMessages.sql003 -f $nbm)
                $sql_ok = $true
            } else {
                Write-Warning ($ModuleMessages.sql002 -f $path)
            }
            $dbc_sql.Close()
            $dbc_sql.Dispose()
            [gc]::Collect()
        }
        if ($sql_ok) {
            $res = $path
        }
    }
    return $res
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Backup-FirefoxBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named File')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$PlacesFile,
        [Parameter(Mandatory=$false)][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$OutPath = (Get-Location -PSProvider FileSystem)
    )
    $res = $null
    if ($DefaultProfile) {
        $source = Get-FirefoxBookmarkLocation -DefaultProfile
    } else {
        $source = Get-FirefoxBookmarkLocation -PlacesFile $PlacesFile
    }
    if (![string]::IsNullOrWhiteSpace($source)) {
        Write-Verbose ($ModuleMessages.bfb001 -f $source)
        if (Test-Path $OutPath -PathType Container) {
            $backup_file = "$($OutPath)\$(Get-Date -Format "yyyyMMdd_HHmmss")_$(Split-Path $source -Leaf).zip"
            try {
                Compress-Archive -Path $source -DestinationPath $backup_file -EA Stop
                $res = $backup_file
            } catch {
                Write-Warning ($ModuleMessages.bfb003 -f $_.Exception.Message,$backup_file)
            }
        } else {
            Write-Warning ($ModuleMessages.bfb002 -f $OutPath)
        }
    }
    return $res
}


function ConvertFrom-MozillaTime {
    Param(
        [Parameter(Mandatory=$true)][int64]$MozillaTime
    )
    <#
        This is a helper function to convert a long integer Mozilla time to [datetime]
    #>

    $origin = [datetime]'1970-01-01 00:00:00'
    $origin.AddMilliSeconds($MozillaTime / 1000)
}


function ConvertTo-MozillaTime {
    Param(
        [Parameter(Mandatory=$true)][datetime]$RealTime
    )
    <#
        This is a helper function to convert [datetime] to a long integer Mozilla time
    #>

    $origin = [datetime]'1970-01-01 00:00:00'
    ((New-TimeSpan -Start $origin -End $RealTime).TotalMilliseconds -as [int64]) * 1000
}


function Get-MozillaHash {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)][string]$Url
    )
    <#
        This is a helper function to generate URL hashes. The procedure is described in
        https://github.com/bencaradocdavies/sqlite-mozilla-url-hash/blob/master/hash.c
        we don't generate a hash as of right now but will explore it further in the future
        Since PowerShell isn't very good at handling unsigned integers, it will probably
        have to be done in c#
    #>

    return 0
}


function Get-MozillaGUID {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false)][int]$GUIDLength = 12
    )
    <#
        This is a helper function to generate GUIDs for Mozilla tabels.
        The uniqueness is not checked nor enforced here so needs to be checked in the calling routine.
    #>

    $ValidChars = "0123456789-abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray()
    $moz_guid = ""
    for ($i=1; $i -le $GUIDLength; $i++) {
        $moz_guid += $ValidChars | Get-Random 
    }
    return $moz_guid
}


function Get-MozillaURLMeta {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)][ValidateScript({if ($_ -as [uri]) { $true }})][string]$URL
    )
    <#
        This is a helper function to generate URL metadata needed by Mozilla:
        - prefix (http://, https:// etc.)
        - host (FQDN, hostname or IP address of the host, basically everything between the prefix and the first slash)
        - revhost (the value of host in reverse character order
    #>

    $out = [PSCustomObject]@{
        'prefix' = $URL.Substring(0,$URL.IndexOf("://") + 3)
        'host' = ($URL.Substring($URL.IndexOf("://") + 3) -split "/")[0]
        'revhost' = ''
    }
    for ($i = $out.host.Length - 1; $i -ge 0; $i--) {
        $out.revhost += $out.host.Substring($i,1)
    }
    $out.revhost += "."
    $out
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Get-FirefoxBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named File')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$SourceFile,
        [Parameter(Mandatory=$false)][switch]$ExcludeFolders
    )
    if ($script:SQLiteLibPath) {
        try {
            Add-Type -Path $script:SQLiteLibPath -EA Stop
        } catch {
            Write-Warning ($moduleMessages.init02 -f $_.Exception.Message)
            return
        }
    } else {
        return
    }
        
    if ($DefaultProfile) {
        $src_file = Get-FirefoxBookmarkLocation -DefaultProfile
    } else {
        $src_file = Get-FirefoxBookmarkLocation -PlacesFile $SourceFile
    }

    if ($src_file) {
        $src_ok = $false
        $dbc_src = New-Object -TypeName System.Data.SQLite.SQLiteConnection
        $dbc_src.ConnectionString = "Data Source=$src_file"
        try {
            $dbc_src.Open()
        } catch {
            Write-Warning ($moduleMessages.sql001 -f $_.Exception.Message)
        }
        if ($dbc_src.State -eq "Open") {
            $cmd_src = $dbc_src.CreateCommand()
            $dta_src = New-Object System.Data.SQLite.SQLiteDataAdapter $cmd_src
            $dts_src = New-Object System.Data.DataSet
            $cmd2_src = $dbc_src.CreateCommand()
            $cmd_src.CommandText = "SELECT COUNT(*) FROM moz_bookmarks WHERE type <> 1"
            if ($cmd_src.ExecuteScalar() -gt 0) {
                $src_ok = $true
            } else {
                Write-Warning ($ModuleMessages.gfb002 -f $src_file)
            }
            if ($src_ok) {
                if (!$ExcludeFolders) {
                    $cmd_src.CommandText = "SELECT * FROM moz_bookmarks WHERE type = 2 AND parent > 1 ORDER BY id"
                    $null = $dta_src.Fill($dts_src)
                    $src_folders = @{}
                    foreach ($fld in $dts_src.Tables[0]) {
                        Write-Verbose "Adding folder: $($fld['title'])"
                        if ($src_folders.ContainsKey($fld['parent'])) {
                            $fld_path = "$($src_folders[$fld['parent']]['path'])\$($fld['title'])"
                        } else {
                            $fld_path = $fld['title']
                        }
                        $src_folders.Add($fld['id'],@{'name' = $fld['title'];'parent' = $fld['parent'];'path' = $fld_path})
                    }
                    $dts_src.Dispose()
                }
                
                $cmd_src.CommandText = "SELECT * FROM moz_bookmarks WHERE type = 1"
                $null = $dta_src.Fill($dts_src)
                $bookmarks = $dts_src.Tables[0]
                $dts_src.Dispose()
                foreach ($bmk in $bookmarks) {
                    $bmk_ok = $true
                    $bmk_obj = New-Object BrowserBookmark ('mozilla')
                    $bmk_obj.Title = $bmk['title']
                    $bmk_obj.BMKGuid = $bmk['guid']
                    if ([System.DBNull]::Value -ne $bmk['position']) {
                        $bmk_obj.Order = $bmk['position']
                    }
                    $bmk_obj.DateAdded = ConvertFrom-MozillaTime $bmk['dateAdded']
                    if (!$ExcludeFolders -and $bmk['parent']) {
                        $bmk_obj.Path = $src_folders[$bmk['parent']].path
                    }
                    if ($bmk['fk'] -ne [DBNull]::Value) {
                        $cmd2_src.CommandText = "SELECT * FROM moz_places WHERE id = $($bmk['fk'])"
                        $dta2_src = New-Object System.Data.SQLite.SQLiteDataAdapter $cmd2_src
                        $dts2_src = New-Object System.Data.DataSet
                        $null = $dta2_src.Fill($dts2_src)
                        if ($dts2_src.Tables[0].Rows.Count -eq 0) {
                            Write-Verbose ($ModuleMessages.gfb003 -f $bmk['fk'])
                            $bmk_ok = $false
                        } else {
                            $bmk_obj.URL = $dts2_src.Tables[0].Rows[0]['url']
                            if ([DBNull]::Value -eq $bmk_obj.URL) { $bmk_obj.URL = $null }
                            if ([string]::IsNullOrWhiteSpace($bmk_obj.URL)) {
                                $bmk_ok = $false
                            } else {
                                if ([DBNull]::Value -ne $dts2_src.Tables[0].Rows[0]['description']) {
                                    $desc = $dts2_src.Tables[0].Rows[0]['description']
                                    if ($desc) { $bmk_obj.Description = $desc.Trim() }
                                }
                                $bmk_obj.URLGuid = $dts2_src.Tables[0].Rows[0]['guid']
                                $bmk_obj.URLHash = $dts2_src.Tables[0].Rows[0]['url_hash']
                            }
                        }
                        $dts2_src.Dispose()
                    }
                    if ($bmk_ok) {
                        $bmk_obj
                    }
                }
            }
        }
        if ($dbc_src.State -eq "Open") {
            Write-Verbose $ModuleMessages.gfb005
            # this is necessary to free up SQLite handles
            $cmd_src.Dispose()
            $cmd2_src.Dispose()
            $dbc_src.Close()
            $dbc_src.Dispose()
            [gc]::Collect()
        }
    } else {
        Write-Warning ($ModuleMessages.gfb004 -f $src_file)
    }
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Add-FirefoxBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)][object[]]$Bookmark,
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named File')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$TargetFile,
        [Parameter(Mandatory=$false)][switch]$ExcludeFolders,
        [Parameter(Mandatory=$false)][switch]$BackupTarget,
        [Parameter(Mandatory=$false)][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$BackupPath = (Get-Location -PSProvider FileSystem)
    )
    Begin {
        if ($script:SQLiteLibPath) {
            try {
                Add-Type -Path $script:SQLiteLibPath -EA Stop
            } catch {
                Write-Warning ($moduleMessages.init02 -f $_.Exception.Message)
                return
            }
        } else {
            return
        }
        $out = New-Object BookmarkOperationResult ('mozilla')
        
        $tgt_folders = @{}
        $root_folder_id = $null
        $new_file = $false
        $existing_bookmarks = @()
        if ($DefaultProfile) {
            $target_file = Get-FirefoxBookmarkLocation -DefaultProfile
        } else {
            $target_file = Get-FirefoxBookmarkLocation -PlacesFile $TargetFile
            if ([string]::IsNullOrWhiteSpace($target_file)) {
                try {
                    Write-Verbose ($moduleMessages.afb001 -f $TargetFile)
                    Copy-Item -Path "$PSScriptRoot\lib\places.sqlite" -Destination $TargetFile -Force -EA Stop
                    $target_file = $TargetFile
                    $new_file = $true
                } catch {
                    Write-Warning ($moduleMessages.afb004 -f $_.Exception.Message)
                    $out.ErrorsEncountered++
                    $out
                    break
                }
            }
        }

        if ($target_file -and !$new_file) {
            if ($BackupTarget) {
                $backup_file = Backup-FirefoxBookmark -PlacesFile $target_file -OutPath $BackupPath
                if ($backup_file) {
                    Write-Verbose ($ModuleMessages.afb005 -f $backup_file)
                    $out.BackupFile = $backup_file
                } else {
                    Write-Warning ($ModuleMessages.afb006)
                    $out.ErrorsEncountered++
                    $out
                    break
                }
            }
            $existing_bookmarks = Get-FirefoxBookmark -SourceFile $target_file
            if ($existing_bookmarks) {
                Write-Verbose ($ModuleMessages.afb002 -f $existing_bookmarks.Count)
                $existing_urls = $existing_bookmarks | Select-Object -ExpandProperty url
            } else {
                Write-Verbose $ModuleMessages.afb003
                $existing_urls = @()
            }
        }
        if ($target_file) {
            $out.TargetPath = $target_file
            $dbc_tgt = New-Object -TypeName System.Data.SQLite.SQLiteConnection
            $dbc_tgt.ConnectionString = "Data Source=$target_file"
            try {
                $dbc_tgt.Open()
                $cmd_tgt = $dbc_tgt.CreateCommand()
                $dta_tgt = New-Object System.Data.SQLite.SQLiteDataAdapter $cmd_tgt
                $dts_tgt = New-Object System.Data.DataSet
            } catch {
                Write-Warning ($moduleMessages.afb007 -f $_.Exception.Message)
                $out.ErrorsEncountered++
                $out
                break
            }
            Write-Verbose $moduleMessages.afb008
            $cmd_tgt.CommandText = "SELECT id FROM moz_bookmarks WHERE type = 2 AND guid='toolbar_____'"
            $root_folder_id = $cmd_tgt.ExecuteScalar()
            if (!$root_folder_id) {
                Write-Verbose $moduleMessages.afb009
                $cmd_tgt.CommandText = "SELECT id FROM moz_bookmarks WHERE type = 2 AND guid='menu________'"
                $root_folder_id = $cmd_tgt.ExecuteScalar()
            }
            if (!$root_folder_id) {
                Write-Verbose $moduleMessages.afb010
                $cmd_tgt.CommandText = "SELECT id FROM moz_bookmarks WHERE type = 2 AND guid='unfiled_____'"
                $root_folder_id = $cmd_tgt.ExecuteScalar()
            }
            if (!$root_folder_id) {
                Write-Verbose $moduleMessages.afb011
                $cmd_tgt.CommandText = "SELECT MIN(id) FROM moz_bookmarks WHERE type = 2 AND parent=1"
                $root_folder_id = $cmd_tgt.ExecuteScalar()
            }
            if (!$root_folder_id) {
                Write-Warning $moduleMessages.afb012
                if ($dbc_tgt.State -eq 'Open') {
                    Write-Verbose $moduleMessages.sql004
                    $cmd_tgt.Dispose()
                    $dbc_tgt.Close()
                }
                $out
                break
            }
            Write-Verbose ($moduleMessages.afb013 -f $root_folder_id)

            Write-Verbose $moduleMessages.afb014
            if (!$ExcludeFolders) {
                $cmd_tgt.CommandText = "SELECT * FROM moz_bookmarks WHERE type = 2 AND parent >= $root_folder_id ORDER BY id"
                $null = $dta_tgt.Fill($dts_tgt)
                foreach ($fld in $dts_tgt.Tables[0]) {
                    Write-Verbose "Adding folder: $($fld['title'])"
                    if ($tgt_folders.ContainsKey($fld['parent'])) {
                        $fld_path = "$($tgt_folders[$fld['parent']]['path'])\$($fld['title'])"
                    } else {
                        $fld_path = $fld['title']
                    }
                    $tgt_folders.Add($fld['id'],@{'name' = $fld['title'];'parent' = $fld['parent'];'path' = $fld_path})
                }
                $dts_tgt.Dispose()
            }
        }
    }
    Process {
        foreach ($bm in $Bookmark) {
            if ([string]::IsNullOrWhiteSpace($bm.url)) {
                Write-Verbose $moduleMessages.url001
                continue
            }
            if ($existing_urls -contains $bm.url) {
                Write-Verbose ($moduleMessages.url002 -f $bm.url)
                $out.BookmarksSkipped ++
                continue
            } else {
                Write-Verbose ($moduleMessages.url003 -f $bm.url)
                $bm_meta = Get-MozillaURLMeta -URL $bm.url
                if ($bm.Path -and !$ExcludeFolders) {
                    Write-Verbose ($moduleMessages.afb015 -f $bm.Path)
                    $path_folder = $tgt_folders.GetEnumerator() | where {$_.Value['path'] -eq $bm.Path}
                    if ($path_folder) {
                        Write-Verbose ($moduleMessages.afb016 -f $path_folder.Name)
                        $bm_folder_id = $path_folder.Name
                    } else {
                        Write-Verbose $moduleMessages.afb017
                        $fld_time = ConvertTo-MozillaTime -RealTime (Get-Date)
                        $path_parts = $bm.Path -split "\\"
                        $parent_id = $root_folder_id
                        $full_path = ""
                        foreach ($fld in $path_parts) {
                            Write-Verbose ($moduleMessages.afb018 -f $fld)
                            if ($full_path.Length -gt 0) {
                                $full_path = "$full_path\$fld"
                            } else {
                                $full_path = $fld
                            }
                            Write-Verbose ($moduleMessages.afb019 -f $full_path)
                            $path_folder = $tgt_folders.GetEnumerator() | where {$_.Value['path'] -eq $full_path}
                            if ($path_folder) {
                                Write-Verbose ($moduleMessages.afb016 -f $path_folder.Name)
                                $parent_id = $path_folder.Name
                            } else {
                                $sql_title = $fld -replace "'","''"
                                $cmd_tgt.CommandText = "SELECT MIN(id) AS FolderEx FROM moz_bookmarks WHERE type=2 AND parent=$parent_id AND title='$sql_title'"
                                $existing_folder = $cmd_tgt.ExecuteScalar()
                                if ([DBNull]::Value -ne $existing_folder) {
                                    Write-Verbose ($moduleMessages.afb020 -f $existing_folder)
                                    $parent_id = $existing_folder
                                } else {
                                    do {
                                        $guid = Get-MozillaGUID
                                        $cmd_tgt.CommandText = "SELECT id FROM moz_bookmarks WHERE guid='$guid'"
                                    } until ($null -eq $cmd_tgt.ExecuteScalar())
                                    $cmd_tgt.CommandText = "SELECT MAX(position) FROM moz_bookmarks WHERE type=2 AND parent=$parent_id"
                                    $pos = $cmd_tgt.ExecuteScalar()
                                    Write-Verbose ($moduleMessages.afb021 -f $pos)
                                    if ([DBNull]::Value -ne $pos) {
                                        $pos++
                                    } else {
                                        $pos = 0
                                    }
                                    $q = "INSERT INTO moz_bookmarks (id,type,parent,position,title,guid,dateAdded,lastModified) VALUES (NULL,2,$parent_id,$pos,'$sql_title','$guid',$fld_time,$fld_time)"
                                    Write-Verbose $q
                                    $cmd_tgt.CommandText = $q
                                    $cmd_tgt.ExecuteNonQuery()
                                    $cmd_tgt.CommandText = "select last_insert_rowid()"
                                    $new_parent_id = $cmd_tgt.ExecuteScalar()
                                    $tgt_folders.Add($new_parent_id,@{'name'=$fld;'path'=$full_path;'parent'=$parent_id})
                                    $parent_id = $new_parent_id
                                }
                            }
                        }
                        $bm_folder_id = $parent_id
                    }
                } else {
                    $bm_folder_id = $root_folder_id
                }
            }
            # check or add origin
            $q = "SELECT MIN(id) FROM moz_origins WHERE prefix='$($bm_meta.prefix)' AND host='$($bm_meta.host)'"
            $cmd_tgt.CommandText = $q
            $orgid = $cmd_tgt.ExecuteScalar()
            if ([DBNull]::Value -ne $orgid) {
                $bm_origin_id = $orgid
                Write-Verbose ($moduleMessages.afb022 -f $bm_origin_id)
            } else {
                $cmd_tgt.CommandText = "INSERT INTO moz_origins (id,prefix,host,frecency) VALUES (NULL,'$($bm_meta.prefix)','$($bm_meta.host)',0)"
                $cmd_tgt.ExecuteNonQuery()
                $cmd_tgt.CommandText = "select last_insert_rowid()"
                $bm_origin_id = $cmd_tgt.ExecuteScalar()
                Write-Verbose ($moduleMessages.afb023 -f $bm_origin_id)
            }

            # add url
            do {
                $guid = Get-MozillaGUID
                $cmd_tgt.CommandText = "SELECT id FROM moz_places WHERE guid='$guid'"
            } until ($null -eq $cmd_tgt.ExecuteScalar())
            $url_hash = Get-MozillaHash -Url $bm.URL
            $cmd_tgt.CommandText = "INSERT INTO moz_places (id,url,rev_host,guid,description,url_hash,origin_id,frecency) VALUES (NULL,'$($bm.URL)','$($bm_meta.revhost)','$guid','$($bm.Description -replace "'","''")',$url_hash,$bm_origin_id,0)"
            $cmd_tgt.ExecuteNonQuery()
            $cmd_tgt.CommandText = "select last_insert_rowid()"
            $bm_url_id = $cmd_tgt.ExecuteScalar()
            Write-Verbose ($moduleMessages.afb024 -f $bm_url_id)

            # add bookmark
            $sql_title = $bm.Title -replace "'","''"
            $time_da = ConvertTo-MozillaTime $bm.DateAdded
            $time_lm = ConvertTo-MozillaTime (Get-Date)
            do {
                $guid = Get-MozillaGUID
                $cmd_tgt.CommandText = "SELECT id FROM moz_bookmarks WHERE guid='$guid'"
            } until ($null -eq $cmd_tgt.ExecuteScalar())
            if ($null -eq $bm_folder_id) { $bm_folder_id = 'NULL' }
            $q = "INSERT INTO moz_bookmarks (id,type,guid,parent,fk,title,dateAdded,lastModified) VALUES (NULL,1,'$guid',$bm_folder_id,$bm_url_id,'$sql_title',$time_da,$time_lm)"
            $cmd_tgt.CommandText = $q
            $cmd_tgt.ExecuteNonQuery()
            $cmd_tgt.CommandText = "select last_insert_rowid()"
            $bm_id = $cmd_tgt.ExecuteScalar()
            Write-Verbose ($moduleMessages.afb025 -f $bm_id)
            $out.BookmarksAdded++
        }
    }
    End {
        if ($dbc_tgt.State -eq 'Open') {
            Write-Verbose $moduleMessages.sql004
            $cmd_tgt.Dispose()
            $dbc_tgt.Close()
            $dbc_tgt.Dispose()
            [gc]::Collect()
        }
        $out
    }
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Remove-FirefoxBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)][ValidateScript({if ($_ -as [uri]) { $true }})][string]$URL,
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named File')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$TargetFile,
        [Parameter(Mandatory=$false)][switch]$BackupTarget,
        [Parameter(Mandatory=$false)][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$BackupPath = (Get-Location -PSProvider FileSystem)
    )
    Write-Warning "Remove-FireFoxBookmark has not been implemented yet"
    if ($script:SQLiteLibPath) {
        try {
            Add-Type -Path $script:SQLiteLibPath -EA Stop
        } catch {
            Write-Warning ($moduleMessages.init02 -f $_.Exception.Message)
            return
        }
    } else {
        return
    }
    $out = New-Object BookmarkOperationResult ('mozilla')

    return $out
}
#endregion

#region internet explorer

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Get-IEBookmarkLocation {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named Folder')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$SourceFolder
    )
    $res = $null
    if ($DefaultProfile) {
        $fav_path = Get-ItemPropertyValue -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" -Name "Favorites" -EA SilentlyContinue
        if ($fav_path) {
            Write-Verbose ($ModuleMessages.gil002 -f $fav_path)
            $path = $fav_path
        } else {
            Write-Warning $ModuleMessages.gil001
        }
    } else {
        $SourceFolder = $SourceFolder.Trim().TrimEnd("\")
        if (Test-Path $SourceFolder -PathType Container) {
            $path = $SourceFolder
        }  
    }
    if ($path) {
        $url_files = @(Get-ChildItem -Path $path -Filter "*.url" -Recurse)
        if ($url_files.Count -gt 0) { $res = $path }
    }
    return $res
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Backup-IEBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named File')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$SourceFolder,
        [Parameter(Mandatory=$false)][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$OutPath = (Get-Location -PSProvider FileSystem)
    )
    $res = $null
    if ($DefaultProfile) {
        $source = Get-IEBookmarkLocation -DefaultProfile
    } else {
        $source = Get-IEBookmarkLocation -SourceFolder $SourceFolder
    }
    if (![string]::IsNullOrWhiteSpace($source)) {
        Write-Verbose ($ModuleMessages.bib001 -f $source)
        if (Test-Path $OutPath -PathType Container) {
            $backup_file = "$($OutPath)\$(Get-Date -Format "yyyyMMdd_HHmmss")_$(Split-Path $source -Leaf).zip"
            try {
                Compress-Archive -Path $source -DestinationPath $backup_file -EA Stop
                $res = $backup_file
            } catch {
                Write-Warning ($ModuleMessages.bib003 -f $_.Exception.Message,$backup_file)
            }
        } else {
            Write-Warning ($ModuleMessages.bib002 -f $OutPath)
        }
    }
    $res
}


function ConvertTo-FileName {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)][string]$Title
    )
    <#
        This is a helper function to build a filename (for the .url or .website file) out of the page title
    #>

    $title_sanitized = $Title -replace '(\*|\\|\/|\?|\<|\>|\:|\||\")','-'
    $title_sanitized.Substring(0,[math]::Min($title_sanitized.Length,250))
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Get-IEBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named Folder')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$SourceFolder,      
        [Parameter(Mandatory=$false)][switch]$ExcludeFolders
    )
    if ($DefaultProfile) {
        $fav_folder = Get-IEBookmarkLocation -DefaultProfile
    } else {
        $fav_folder = Get-IEBookmarkLocation -SourceFolder $SourceFolder
    }
    if ($fav_folder) {
        Get-ChildItem -Path $fav_folder -Filter "*.url" -Recurse | ForEach-Object {
            Write-Verbose $_.FullName
            $bmk_ok = $true
            $bmk_obj = New-Object BrowserBookmark ('ie')
            $bmk_obj.DateAdded = $_.CreationTime
            $bmk_obj.Title = $_.Name.Substring(0,$_.Name.LastIndexOf("."))
            $bmkini = Get-IniContent $_.FullName
            if ($bmkini.Count -gt 0) {
                if ($bmkini['InternetShortcut']['URL'] -as [uri]) {
                    # description field
                    if ($bmkini['{5CBF2787-48CF-4208-B90E-EE5E5D420294}']) {
                        $desc_line = $bmkini['{5CBF2787-48CF-4208-B90E-EE5E5D420294}']["Prop21"]
                        if ($desc_line) {
                            if ($desc_line.Substring(0,3) -eq '31,') {
                                $bmk_obj.Description = $desc_line.Substring(3)
                            } else {
                                Write-Verbose ($ModuleMessages.gib003 -f $($desc_line.Substring(0,3)))
                            }
                        } else {
                            Write-Verbose ($ModuleMessages.gib006)
                        }
                    }
                    # notes field
                    if ($bmkini['{B9B4B3FC-2B51-4A42-B5D8-324146AFCF25}']) {
                        $desc_line = $bmkini['{B9B4B3FC-2B51-4A42-B5D8-324146AFCF25}']["Prop5"]
                        if ($desc_line) {
                            if ($desc_line.Substring(0,3) -eq '31,') {
                                $bmk_obj.Notes = $desc_line.Substring(3)
                            } else {
                                Write-Verbose ($ModuleMessages.gib004 -f $($desc_line.Substring(0,3)))
                            }
                        } else {
                            Write-Verbose ($ModuleMessages.gib007)
                        }
                    }
                    # stars field
                    if ($bmkini['{64440492-4C8B-11D1-8B70-080036B11A03}']) {
                        $desc_line = $bmkini['{64440492-4C8B-11D1-8B70-080036B11A03}']["Prop9"]
                        if ($desc_line) {
                            if ($desc_line.Substring(0,3) -eq '19,') {
                                switch ($desc_line.Substring(3) -as [int]) {
                                    1 { $bmk_obj.Stars = 1 }
                                    25 { $bmk_obj.Stars = 2 }
                                    50 { $bmk_obj.Stars = 3 }
                                    75 { $bmk_obj.Stars = 4 }
                                    99 { $bmk_obj.Stars = 5 }
                                    default { Write-Verbose ($ModuleMessages.gib009 -f ($desc_line.Substring(3) -as [int])) }
                                }
                            } else {
                                Write-Verbose ($ModuleMessages.gib005 -f $($desc_line.Substring(0,3)))
                            }
                        } else {
                            Write-Verbose ($ModuleMessages.gib008)
                        }
                    }
                    $bmk_obj.URL = $bmkini['InternetShortcut']['URL']
                    $bmk_obj.IconFile = $bmkini['InternetShortcut']['iconfile']
                    $bmk_obj.IconIndex = $bmkini['InternetShortcut']['iconindex']
                } else {
                    $bmk_ok  = $false
                }
            } else {
                $bmk_ok  = $false
            }
            if (!$ExcludeFolders -and ((Split-Path $_.FullName -Parent) -notlike $fav_folder)) {
                $bmk_obj.Path = (Split-Path $_.FullName -Parent).Substring($fav_folder.Length + 1)
            }
            if ($bmk_ok) {
                $bmk_obj
            }
        }
    }
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Add-IEBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)][object[]]$Bookmark,
        [Parameter(Mandatory=$false,ParameterSetName="Default")][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName="Named Folder")][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$TargetFolder,
        [Parameter(Mandatory=$false)][switch]$ExcludeFolders,
        [Parameter(Mandatory=$false)][switch]$BackupTarget,
        [Parameter(Mandatory=$false)][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$BackupPath = (Get-Location -PSProvider FileSystem)
    )
    # construction: https://support.microsoft.com/en-us/help/2568750/apply-property-error-occurs-when-attempting-to-modify-either-the-ratin
    Begin {
        $out = New-Object BookmarkOperationResult ('ie')
        if ($DefaultProfile) {
            $tgt_folder = Get-DefaultIEFaves
        } else {
            $tgt_folder = $TargetFolder.Trim().TrimEnd("\")
            if (!(Test-Path $tgt_folder -PathType Container)) {
                Write-Verbose ($ModuleMessages.aib001 -f $tgt_folder)
                try {
                    $null = New-Item $tgt_folder -ItemType Directory -Force -EA Stop
                } catch {
                    Write-Warning ($ModuleMessages.aib003 -f $_.Exception.Message)
                }
            }
        }
        if (Test-Path $tgt_folder -PathType Container) {
            $out.TargetPath = $tgt_folder
            $existing_bookmarks = Get-IEBookmark -SourceFolder $tgt_folder
            if ($existing_bookmarks) {
                Write-Verbose ($ModuleMessages.aib004 -f $existing_bookmarks.Count)
                $existing_urls = $existing_bookmarks | Select-Object -ExpandProperty url
                if ($BackupTarget) {
                    $backup_file = Backup-IEBookmark -SourceFolder $tgt_folder
                    $out.BackupFile = $backup_file
                }
            } else {
                Write-Verbose $ModuleMessages.aib006
                $existing_urls = @()
            }
        } else {
            Write-Warning ($ModuleMessages.aib002 -f $tgt_folder)
            $out.ErrorsEncountered ++
            break
        }
    }
    Process {
        foreach ($bm in $Bookmark) {
            if ([string]::IsNullOrWhiteSpace($bm.url)) {
                Write-Verbose $ModuleMessages.aib012
                continue
            }
            if ($existing_urls -contains $bm.url) {
                Write-Verbose ($ModuleMessages.aib011 -f $bm.url)
                $out.BookmarksSkipped ++
                continue
            } else {
                $new_filename = ConvertTo-FileName $bm.title
                if (!$ExcludeFolders -and $bm.path) {
                    $new_path = "$tgt_folder\$($bm.path)"
                } else {
                    $new_path = $tgt_folder
                }
                if (!(Test-Path $new_path -PathType Container)) { 
                    try {
                        $null = New-Item $new_path -ItemType Directory -Force -EA Stop
                    } catch {
                        Write-Warning ($ModuleMessages.aib007 -f $new_path,$_.Exception.Message)
                    }
                }
                if (Test-Path $new_path -PathType Container) {
                    $new_file = "$($new_path)\$($new_filename).url"
                    $fi = 0
                    While (Test-Path $new_file -PathType Leaf) {
                        Write-Warning ($ModuleMessages.aib010 -f $new_file)
                        $fi++
                        $new_file = "$($new_path)\$($new_filename)($fi).url"
                    }
                } else {
                    Write-Warning ($ModuleMessages.aib008 -f $new_path)
                    $new_file = $null
                }
                if ($new_file) {
                    $bkdata = @('[{000214A0-0000-0000-C000-000000000046}]',
                    'Prop3=19,0',
                    '[InternetShortcut]',
                    'IDList=',
                    "URL=$($bm.url)",
                    'Roamed=-1')
                    if (![string]::IsNullOrWhiteSpace($bm.IconFile)) {
                        $bkdata += "IconFile=$($bm.IconFile)"
                        $bkdata += "IconIndex=$($bm.IconIndex)"
                    }
                    if (![string]::IsNullOrWhiteSpace($bm.Description)) {
                        $bkdata += "[{5CBF2787-48CF-4208-B90E-EE5E5D420294}]"
                        $bkdata += "Prop21=31,$($bm.Description)"
                    }
                    if (![string]::IsNullOrWhiteSpace($bm.Notes)) {
                        $bkdata += "[{B9B4B3FC-2B51-4A42-B5D8-324146AFCF25}]"
                        $bkdata += "Prop5=31,$($bm.Notes)"
                    }
                    if ($bm.Stars -gt 0){
                        $bkdata += "[{64440492-4C8B-11D1-8B70-080036B11A03}]"
                        switch ($bm.Stars) {
                            1 { $bkdata += "Prop9=19,1" }
                            2 { $bkdata += "Prop9=19,25" }
                            3 { $bkdata += "Prop9=19,50" }
                            4 { $bkdata += "Prop9=19,75" }
                            5 { $bkdata += "Prop9=19,99" }
                        }
                    }
                    try {
                        $bkdata | Set-Content -LiteralPath "$new_file" -Force -EA Stop
                        $file_ok = $true
                        $out.BookmarksAdded ++
                    } catch {
                        Write-Warning ($ModuleMessages.aib013 -f $new_file, $_.Exception.Message)
                        $file_ok = $false
                        $out.ErrorsEncountered ++
                    }
                    if ($file_ok -and $bm.DateAdded) {
                        Write-Verbose ($ModuleMessages.aib009 -f $new_file,(Get-Date $bm.DateAdded -Format "dd.MM.yyyy"))
                        try {
                            (Get-Item -LiteralPath "$new_file").LastWriteTime = $bm.DateAdded
                        } catch {
                            Write-Warning ($ModuleMessages.aib014 -f $new_file, $_.Exception.Message)
                        }
                    }
                }
            }
        }
    }
    End {
        $out
    }
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Remove-IEBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)][ValidateScript({if ($_ -as [uri]) { $true }})][string]$URL,
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named Folder')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$TargetFolder,
        [Parameter(Mandatory=$false)][switch]$BackupTarget,
        [Parameter(Mandatory=$false)][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$BackupPath = (Get-Location -PSProvider FileSystem)
    )
    Write-Warning "Remove-IEBookmark has not been implemented yet"
    $out = New-Object BookmarkOperationResult ('ie')

    return $out
}
#endregion

#region chrome

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Get-ChromeBookmarkLocation {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named File')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$SourceFile
    )
    #2do: Expand placeholders
    $res = $null
    if ($DefaultProfile) {
        $file = "$($env:LOCALAPPDATA)\Google\Chrome\User Data\Default\Bookmarks"
        $sync_disabled = $false
        $sync_disabled_machine = $false
        if (Test-Path "HKLM:\Software\Policies\Google\Chrome") {
            if (Get-ItemProperty -Path "HKLM:\Software\Policies\Google\Chrome" -Name "SyncDisabled") {
                $sync_disabled_machine = $true
                if ((Get-ItemPropertyValue -Path "HKLM:\Software\Policies\Google\Chrome" -Name "SyncDisabled") -eq 1) {
                    Write-Verbose $ModuleMessages.gcl001
                    $sync_disabled = $true
                }
            }
        } 
        if (!$sync_disabled_machine) {
            if (Test-Path "HKCU:\Software\Policies\Google\Chrome") {
                if (Get-ItemProperty -Path "HKCU:\Software\Policies\Google\Chrome" -Name "SyncDisabled") {
                    if ((Get-ItemPropertyValue -Path "HKCU:\Software\Policies\Google\Chrome" -Name "SyncDisabled") -eq 1) {
                        Write-Verbose $ModuleMessages.gcl002
                        $sync_disabled = $true
                    }
                }
            } 
        }
        if (!$sync_disabled) {
            Write-Verbose "Sync is not disabled, looking for Roaming Profile configuration..."
            $rp_support = $false
            $rp_support_machine = $false
            if (Test-Path "HKLM:\Software\Policies\Google\Chrome") {
                if (Get-ItemProperty -Path "HKLM:\Software\Policies\Google\Chrome" -Name "RoamingProfileSupportEnabled") {
                    $rp_support_machine = $true
                    if ((Get-ItemPropertyValue -Path "HKLM:\Software\Policies\Google\Chrome" -Name "RoamingProfileSupportEnabled") -eq 1) {
                        Write-Verbose $ModuleMessages.gcl003
                        $rp_support = $true
                    }
                }
            }
            if (!$rp_support_machine) {
                if (Test-Path "HKCU:\Software\Policies\Google\Chrome") {
                    if (Get-ItemProperty -Path "HKCU:\Software\Policies\Google\Chrome" -Name "RoamingProfileSupportEnabled") {
                        if ((Get-ItemPropertyValue -Path "HKCU:\Software\Policies\Google\Chrome" -Name "RoamingProfileSupportEnabled") -eq 1) {
                            Write-Verbose $ModuleMessages.gcl004
                            $rp_support = $true
                        }
                    }
                }
            }
        }
        if ($rp_support) {
            $file = "$($env:APPDATA)\Google\Chrome\User Data\Default\Bookmarks"
            # 2do: Finisch expansion of profile paths
            # https://www.chromium.org/administrators/policy-list-3/user-data-directory-variables
        } else {
            Write-Verbose $ModuleMessages.gcl005

        }
        if (Test-Path $file -PathType Leaf) {
            $path = $file
        } else {
            Write-Verbose ("File not found" -f $file)
        }
    } else {
        $SourceFile = $SourceFile.Trim()
        if (Test-Path $SourceFile -PathType Leaf) {
            $path = $SourceFile
        }
    }
    if ($path) {
        $bm_struct = $null
        $chrome_bm_cnt = Get-Content $path 
        try {
            $bm_struct = $chrome_bm_cnt | ConvertFrom-Json -EA Stop
        } catch {
            Write-Warning ($ModuleMessages.gcl006 -f $_.Exception.Message)
        }
        if ($bm_struct) {
            if ($bm_struct.roots.bookmark_bar.children.Count -gt 0) {
                $res = $path
            }
        }
    }
    return $res
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Backup-ChromeBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named File')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$SourceFile,
        [Parameter(Mandatory=$false)][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$OutPath = (Get-Location -PSProvider FileSystem)
    )
    $res = $null
    if ($DefaultProfile) {
        $source = Get-ChromeBookmarkLocation -DefaultProfile
    } else {
        $source = Get-ChromeBookmarkLocation -SourceFile $SourceFile
    }
    if (![string]::IsNullOrWhiteSpace($source)) {
        Write-Verbose ($ModuleMessages.bcb001 -f $source)
        if (Test-Path $OutPath -PathType Container) {
            $backup_file = "$($OutPath)\$(Get-Date -Format "yyyyMMdd_HHmmss")_$(Split-Path $source -Leaf).zip"
            try {
                Compress-Archive -Path $source -DestinationPath $backup_file -EA Stop
                $res = $backup_file
            } catch {
                Write-Warning ($ModuleMessages.bcb003 -f $_.Exception.Message,$backup_file)
            }
        } else {
            Write-Warning ($ModuleMessages.bcb002 -f $OutPath)
        }
    }
    $res
}


function ConvertFrom-ChromeTime {
    Param(
        [Parameter(Mandatory=$true)][string]$ChromeTime
    )
    <#
        This is a helper function to convert a string (Chrome time) to a [datetime]
    #>

    $chrome_normalized = $ChromeTime.PadRight(18,"0")
    if ($chrome_normalized -as [int64]) {
        [datetime]::FromFileTime($chrome_normalized)
    }
}


function ConvertTo-ChromeTime {
    Param(
        [Parameter(Mandatory=$true)][datetime]$RealTime
    )
    <#
        This is a helper function to convert a [datetime] to a 17-digit FileTime (Chrome time)
    #>

    return $RealTime.ToFileTime().ToString().Substring(0,17)
}


function New-ChromeFolder {
    Param(
        [Parameter(Mandatory=$true)][int]$FolderID,
        [Parameter(Mandatory=$true)][string]$FolderName,
        [Parameter(Mandatory=$false)][string]$ChromeTimeAdded,
        [Parameter(Mandatory=$false)][string]$ChromeTimeModified
    )
    <#
        This is a helper function to create a folder object for a Chrome hierarchy
    #>

    $res = New-Object ChromeBookmarkFolder($FolderID,$FolderName)
    if ([string]::IsNullOrWhitespace($ChromeTimeAdded)) {
        $res.date_added = ConvertTo-ChromeTime -RealTime (Get-Date)
    } else {
        $res.date_added = $ChromeTimeAdded
    }
    if ([string]::IsNullOrWhitespace($ChromeTimeModified)) {
        $res.date_modified = ConvertTo-ChromeTime -RealTime (Get-Date)
    } else {
        $res.date_modified = $ChromeTimeModified
    }
    return $res
}


function New-ChromeItem {
    Param(
        [Parameter(Mandatory=$true)][int]$ItemID,
        [Parameter(Mandatory=$true)][string]$ItemName,
        [Parameter(Mandatory=$true)][string]$ItemURI,
        [Parameter(Mandatory=$false)][string]$ChromeTimeAdded
    )
    <#
        This is a helper function to create a bookmark object for a Chrome hierarchy
    #>

    $res = New-Object ChromeBookmarkItem($ItemID,$ItemName,$ItemURI)
    if ([string]::IsNullOrWhitespace($ChromeTimeAdded)) {
        $res.date_added = ConvertTo-ChromeTime -RealTime (Get-Date)
    } else {
        $res.date_added = $ChromeTimeAdded
    }
    return $res
}


function Get-ChromeLastID {
    Param(
        [Parameter(Mandatory=$true)][object]$ChromeStructure
    )
    <#
        This is a helper function to find the highest ID in a Chrome hierarchy
    #>

    function Get-ChromeBranchLastID {
        Param(
            [Parameter(Mandatory=$true)][object]$Branch
        )
        $i = $Branch.id -as [int]
        if ($Branch.type -eq "folder") {
            foreach ($sub in $Branch.children) {
                if ($sub.id -gt $i) { $i = $sub.id }
                if ($sub.type -eq "folder") {
                    $j = Get-ChromeBranchLastID $sub
                    if ($j -gt $i) { $i = $j }
                }
            }
        }
        $i
    }
    $maxid = -1
    foreach ($struct in $ChromeStructure.GetEnumerator()) {
        $strmaxid = Get-ChromeBranchLastID $struct.Value
        if ($strmaxid -gt $maxid) { $maxid = $strmaxid }
    }
    $maxid
}


function Process-ChromeBookmarkBranch {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)][object]$ChromeStructure
    )
    <#
        This is a helper function to recursively process a branch of a Chrome folder structure
    #>

    $res = $null
    if ($ChromeStructure.type -eq 'folder') {
        $res = New-ChromeFolder -FolderID $ChromeStructure.id -FolderName $ChromeStructure.name
        foreach ($csi in $ChromeStructure.children) {
            if ($csi.type -eq 'url') {
                $res.children += New-ChromeItem -ItemID $csi.id -ItemName $csi.name -ItemURI $csi.url -ChromeTimeAdded $csi.date_added
            } elseif ($csi.type -eq 'folder') {
                $res.children += Process-ChromeBookmarkBranch -ChromeStructure $csi
            }
        }
    } elseif ($ChromeStructure.type -eq 'url') {
        Write-Warning ($ModuleMessages.pcb001 -f $ChromeStructure.name)
    } else {
        Write-Warning ($ModuleMessages.pcb002 -f $ChromeStructure.type)
    } 
    $res
}


function Import-ChromeBookmarkFile {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)][string]$FilePath
    )
    <#
        This is a helper function to read the Chrome XML file into a hashtable of
        Chrome Structure branches
    #>

    $res = $null
    if (Test-Path $FilePath -PathType Leaf) {
        try {
            $bmstruct = Get-Content -Path $FilePath | ConvertFrom-Json -EA Stop
        } catch {
            Write-Warning ($ModuleMessages.icf001 -f $_.Exception.Message)
        }
        if ($bmstruct.Version -eq 1) {
            $res = @{}
            foreach ($root in $bmstruct.roots.PSObject.Properties) {                
                $res.Add($root.Name, (Process-ChromeBookmarkBranch -ChromeStructure $root.Value))
            }
        } else {
            Write-Warning $ModuleMessages.icf002
        }
    } else {
        Write-Warning ($ModuleMessages.icf003 -f $FilePath)
    }
    $res
}


function Read-ChromeBookmarkBranch {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)][object]$Branch,
        [Parameter(Mandatory=$false)][object]$Parent,
        [Parameter(Mandatory=$false)][bool]$ProcessFolders
    )
    <#
        This is a helper function to recursively convert a folder branch of a Chrome
        data structure to BrowserBookmark objects, adding to the Path property as we
        go deeper
    #>

    if ($Parent) { $Parent = "$Parent\" }
    $BranchPath = "$Parent$($Branch.name)"
    Write-Verbose ($ModuleMessages.rcb001 -f $BranchPath)
    if ($Branch.children.Count -gt 0) {
        foreach ($child in $Branch.children) {
            if ($child.type -eq "Folder") {
                Write-Verbose ($ModuleMessages.rcb002 -f $child.name)
                Read-ChromeBookmarkBranch -Branch $child -Parent $BranchPath -ProcessFolders $ProcessFolders
            } else {
                Write-Verbose ($ModuleMessages.rcb003 -f $child.name)
                $bmk_ok = $true
                $bmk_obj = New-Object BrowserBookmark ('chrome')
                $bmk_obj.DateAdded = ConvertFrom-ChromeTime $child.date_added
                $bmk_obj.Title = $child.name
                $bmk_obj.URL = $child.url
                $bmk_obj.BMKGuid = $child.guid
                if ($ProcessFolders) {
                    $bmk_obj.Path = $BranchPath
                }
                $bmk_obj
            }
        }
    }
}


function Export-ChromeBookmarkFile {
    Param(
        [Parameter(Mandatory=$true)][object]$ChromeStructure
    )
    <#
        This is a helper function to convert a Chrome data structure back
        to correct Chrome XM
    #>

    $output = [ordered]@{'checksum' = ''; 'roots' = @{}}
    foreach ($root in $ChromeStructure.GetEnumerator()) {
        $output.roots.Add($root.Name, $root.Value)
    }
    $output.Add('version',1)
    $output | ConvertTo-Json -Depth 100
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Get-ChromeBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named File')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$SourceFile,
        [Parameter(Mandatory=$false)][switch]$ExcludeFolders
    )
    $bm_file = $null
    $bm_struct = $null
    if ($DefaultProfile) {
        $bm_file = Get-ChromeBookmarkLocation -DefaultProfile
    } else {
        $bm_file = Get-ChromeBookmarkLocation -SourceFile $SourceFile
    }
    if ($bm_file) {
        $bm_struct = Import-ChromeBookmarkFile -FilePath $bm_file
    }
    if ($bm_struct) {
        Write-Verbose ($ModuleMessages.gcb001 -f $bm_file)
        if ($ExcludeFolders) { $ProcessFolders = $false } else { $ProcessFolders = $true }
        foreach ($root in $bm_struct.GetEnumerator()) {
            Read-ChromeBookmarkBranch -Branch $root.Value -Parent "" -ProcessFolders $ProcessFolders
        }
    }
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Add-ChromeBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)][object[]]$Bookmark,
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named File')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$TargetFile,
        [Parameter(Mandatory=$false)][switch]$ExcludeFolders,
        [Parameter(Mandatory=$false)][switch]$BackupTarget,
        [Parameter(Mandatory=$false)][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$BackupPath = (Get-Location -PSProvider FileSystem)
    )
    Begin {
        $out = New-Object BookmarkOperationResult ('chrome')
        $new_file = $false
        if ($DefaultProfile) {
            $tgt_file = Get-ChromeBookmarkLocation -DefaultProfile
        } else {
            $tgt_file = Get-ChromeBookmarkLocation -SourceFile $TargetFile
            if (!$tgt_file) {
                Write-Verbose ($ModuleMessages.acb001 -f  $TargetFile)
                try {
                    Copy-Item -Path "$PSScriptRoot\lib\chrome.json" -Destination $TargetFile -Force -EA Stop
                    $tgt_file = $TargetFile
                } catch {
                    Write-Warning ($ModuleMessages.acb002 -f $_.Exception.Message)
                }
            }
        }
        if ($tgt_file) {
            $bk_structure = Import-ChromeBookmarkFile $tgt_file
            $out.TargetPath = $tgt_file
            $newid = (Get-ChromeLastID -ChromeStructure $bk_structure) + 1
            Write-Verbose ($ModuleMessages.acb003 -f $newid)
            $existing_bookmarks = Get-ChromeBookmark -SourceFile $tgt_file
        } else {
            break
        }
        if ($existing_bookmarks) {
            Write-Verbose ($ModuleMessages.acb004 -f $existing_bookmarks.Count)
            $existing_urls = $existing_bookmarks | Select-Object -ExpandProperty url
        } else {
            Write-Verbose $ModuleMessages.acb005
            $existing_urls = @()
        }
    }
    Process {
        foreach ($bm in $Bookmark) {
            if ([string]::IsNullOrWhiteSpace($bm.url)) {
                Write-Verbose $ModuleMessages.acb006
                continue
            } elseif ($existing_urls -contains $bm.url) {
                Write-Verbose ($ModuleMessages.acb007 -f $bm.url)
                $out.BookmarksSkipped++
                continue
            }
            $newitem = New-ChromeItem -ItemID $newid -ItemName $bm.Title -ItemURI $bm.URL -ChromeTimeAdded (ConvertTo-ChromeTime -RealTime $bm.DateAdded)
            $newid++

            if ([string]::IsNullOrWhiteSpace($bm.Path) -or $ExcludeFolders) { 
                Write-Verbose $ModuleMessages.acb008
                $bk_structure['bookmark_bar'].children += $newitem
                $out.BookmarksAdded++
            } elseif (!([string]::IsNullOrWhiteSpace($bm.Path))) {
                $path_parts = $bm.Path -split "\\"
                $cur_parent = $bk_structure['bookmark_bar']
                foreach ($pp in $path_parts) {
                    $pp_fld = $cur_parent.children.Where({($_.name -eq $pp) -and ($_.type = 'folder')})[0]
                    if ($pp_fld) {
                        $cur_parent = $pp_fld
                    } else {
                        Write-Verbose ($ModuleMessages.acb009 -f $pp,$cur_parent.name) 
                        $cur_parent.children += (New-ChromeFolder -FolderID $newid -FolderName $pp)
                        $newid++
                        $cur_parent = $cur_parent.children.Where({($_.name -eq $pp) -and ($_.Type = 'folder')})[0]
                    }
                }
                if ($null -ne $cur_parent) {
                    Write-Verbose ($ModuleMessages.acb010 -f $cur_parent.name)
                    $cur_parent.children += $newitem
                    $out.BookmarksAdded++
                    $newid++
                }
            }
        }
    }
    End {
        Export-ChromeBookmarkFile -ChromeStructure $bk_structure | Set-Content $tgt_file -Force
        $out
    }
}

# .ExternalHelp PSBrowserBookmarks.psm1-help.xml
function Remove-ChromeBookmark {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)][ValidateScript({if ($_ -as [uri]) { $true }})][string]$URL,
        [Parameter(Mandatory=$false,ParameterSetName='Default')][switch]$DefaultProfile,
        [Parameter(Mandatory=$false,ParameterSetName='Named File')][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$TargetFile,
        [Parameter(Mandatory=$false)][switch]$BackupTarget,
        [Parameter(Mandatory=$false)][ValidateScript({$null -ne ($_ -as [System.IO.FileInfo])})][string]$BackupPath = (Get-Location -PSProvider FileSystem)
    )
    Write-Warning "Remove-ChromeBookmark has not been implemented yet"
    $out = New-Object BookmarkOperationResult ('chrome')

    return $out
}
#endregion

#region init
Import-LocalizedData -BindingVariable "ModuleMessages"

if (!(Test-Path "$PSScriptRoot\lib\System.Data.SQLite.dll")) {
    Write-Warning $ModuleMessages.init01
} else {
    New-Variable -Scope Script -Name SQLiteLibPath -Value "$PSScriptRoot\lib\System.Data.SQLite.dll"
}
#endregion