Functions/GenXdev.Webbrowser/Get-BrowserBookmark.ps1

<##############################################################################
Part of PowerShell module : GenXdev.Webbrowser
Original cmdlet filename : Get-BrowserBookmark.ps1
Original author : René Vaessen / GenXdev
Version : 1.264.2025
################################################################################
MIT License
 
Copyright 2021-2025 GenXdev
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
################################################################################>

###############################################################################
<#
.SYNOPSIS
Returns all bookmarks from installed web browsers.
 
.DESCRIPTION
Retrieves bookmarks from Microsoft Edge, Google Chrome, or Mozilla Firefox
browsers installed on the system. The function can filter by browser type and
returns detailed bookmark information including name, URL, folder location, and
timestamps.
 
.PARAMETER Chrome
Retrieves bookmarks specifically from Google Chrome browser.
 
.PARAMETER Edge
Retrieves bookmarks specifically from Microsoft Edge browser.
 
.PARAMETER Firefox
Retrieves bookmarks specifically from Mozilla Firefox browser.
 
.EXAMPLE
Get-BrowserBookmark -Edge | Format-Table Name, URL, Folder
Returns Edge bookmarks formatted as a table showing name, URL and folder.
 
.EXAMPLE
gbm -Chrome | Where-Object URL -like "*github*"
Returns Chrome bookmarks filtered to only show GitHub-related URLs.
#>

function Get-BrowserBookmark {

    [CmdletBinding(DefaultParameterSetName = 'Default')]

    [OutputType([System.Object[]])]
    [Alias('gbm')]
    param (
        ########################################################################
        [Parameter(
            Mandatory = $false,
            Position = 0,
            HelpMessage = 'Returns bookmarks from Google Chrome'
        )]
        [switch] $Chrome,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            Position = 1,
            HelpMessage = 'Returns bookmarks from Microsoft Edge'
        )]
        [switch] $Edge,

        ########################################################################
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Firefox',
            Position = 2,
            HelpMessage = 'Returns bookmarks from Mozilla Firefox'
        )]
        [switch] $Firefox
    )

    begin {
        # load SQLite client assembly
        GenXdev.Helpers\EnsureNuGetAssembly -PackageKey 'System.Data.Sqlite'

        # ensure filesystem module is loaded for path handling
        if (-not (Microsoft.PowerShell.Core\Get-Command -Name GenXdev.FileSystem\Expand-Path -ErrorAction SilentlyContinue)) {
            Microsoft.PowerShell.Core\Import-Module GenXdev.FileSystem
        }

        Microsoft.PowerShell.Utility\Write-Verbose 'Getting installed browsers...'

        # get list of installed browsers for validation
        $Script:installedBrowsers = GenXdev.Webbrowser\Get-Webbrowser

        # if no specific browser selected, use system default
        if (-not $Edge -and -not $Chrome -and -not $Firefox) {

            Microsoft.PowerShell.Utility\Write-Verbose 'No browser specified, detecting default browser...'
            $defaultBrowser = GenXdev.Webbrowser\Get-DefaultWebbrowser

            # set appropriate switch based on default browser
            if ($defaultBrowser.Name -like '*Edge*') {
                $Edge = $true
            }
            elseif ($defaultBrowser.Name -like '*Chrome*') {
                $Chrome = $true
            }
            elseif ($defaultBrowser.Name -like '*Firefox*') {
                $Firefox = $true
            }
            else {
                Microsoft.PowerShell.Utility\Write-Warning 'Default browser is not Edge, Chrome, or Firefox.'
                return
            }
        }
    }


    process {

        # helper function to parse Chromium-based browser bookmarks
        function Get-ChromiumBookmarks {

            [CmdletBinding()]
            [OutputType([System.Object[]])]
            [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
            [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
            [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')]
            param (
                [string] $bookmarksFilePath,
                [string] $rootFolderName,
                [string] $browserName
            )

            if (-not (Microsoft.PowerShell.Management\Test-Path -LiteralPath $bookmarksFilePath)) {
                Microsoft.PowerShell.Utility\Write-Verbose "Bookmarks file not found: $bookmarksFilePath"
                return @()
            }

            # read bookmarks json file
            $bookmarksContent = Microsoft.PowerShell.Management\Get-Content -LiteralPath  $bookmarksFilePath -Raw |
                Microsoft.PowerShell.Utility\ConvertFrom-Json

            $bookmarks = [System.Collections.Generic.List[object]]::new()

            # recursive function to traverse bookmark tree
            function ParseBookmarkFolder {
                param (
                    [pscustomobject] $folder,
                    [string] $parentFolder = ''
                )

                foreach ($item in $folder.children) {
                    if ($item.type -eq 'folder') {
                        ParseBookmarkFolder -Folder $item `
                            -ParentFolder ($parentFolder + '\' + $item.name)
                    }
                    elseif ($item.type -eq 'url') {
                        $null = $bookmarks.Add([pscustomobject]@{
                                Name          = $item.name
                                URL           = $item.url
                                Folder        = $parentFolder
                                DateAdded     = [DateTime]::FromFileTimeUtc(
                                    [int64]$item.date_added
                                )
                                DateModified  = if ($item.PSObject.Properties.Match(
                                        'date_modified')) {
                                    [DateTime]::FromFileTimeUtc(
                                        [int64]$item.date_modified
                                    )
                                }
                                else {
                                    $null
                                }
                                BrowserSource = $browserName
                            })
                    }
                }
            }

            # process each root folder
            ParseBookmarkFolder -Folder $bookmarksContent.roots.bookmark_bar `
                -ParentFolder "$rootFolderName\Bookmarks Bar"
            ParseBookmarkFolder -Folder $bookmarksContent.roots.other `
                -ParentFolder "$rootFolderName\Other Bookmarks"
            ParseBookmarkFolder -Folder $bookmarksContent.roots.synced `
                -ParentFolder "$rootFolderName\Synced Bookmarks"

            return $bookmarks
        }

        # helper function to parse Firefox bookmarks from SQLite
        function Get-FirefoxBookmark {

            [CmdletBinding()]
            [OutputType([System.Object[]])]

            param (
                [string] $placesFilePath,
                [string] $browserName
            )

            if (-not (Microsoft.PowerShell.Management\Test-Path -LiteralPath $placesFilePath)) {
                Microsoft.PowerShell.Utility\Write-Verbose "Firefox places.sqlite not found: $placesFilePath"
                return @()
            }

            $connectionString = "Data Source=$placesFilePath;Version=3;"
            $query = @'
                SELECT
                    b.title,
                    p.url,
                    b.dateAdded,
                    b.lastModified,
                    f.title AS Folder
                FROM moz_bookmarks b
                JOIN moz_places p ON b.fk = p.id
                LEFT JOIN moz_bookmarks f ON b.parent = f.id
                WHERE b.type = 1
'@


            $bookmarks = @()

            try {

                $connection = Microsoft.PowerShell.Utility\New-Object System.Data.Sqlite.SQLiteConnection($connectionString)
                $connection.Open()
                $command = $connection.CreateCommand()
                $command.CommandText = $query
                $reader = $command.ExecuteReader()

                while ($reader.Read()) {
                    $bookmarks += [pscustomobject]@{
                        Name          = $reader['title']
                        URL           = $reader['url']
                        Folder        = $reader['Folder']
                        DateAdded     = [DateTime]::FromFileTimeUtc($reader['dateAdded'])
                        DateModified  = [DateTime]::FromFileTimeUtc($reader['lastModified'])
                        BrowserSource = $browserName
                    }
                }

                $reader.Close()
                $connection.Close()
            }
            catch {
                Microsoft.PowerShell.Utility\Write-Host "Error reading Firefox bookmarks: $PSItem"
            }

            return $bookmarks
        }

        Microsoft.PowerShell.Utility\Write-Verbose 'Processing browser selection...'

        if ($Edge) {
            # validate Edge installation
            $browser = $Script:installedBrowsers |
                Microsoft.PowerShell.Core\Where-Object { $PSItem.Name -like '*Edge*' }

            if (-not $browser) {
                Microsoft.PowerShell.Utility\Write-Warning 'Microsoft Edge is not installed.'
                return
            }

            # construct path to Edge bookmarks file
            $bookmarksFilePath = Microsoft.PowerShell.Management\Join-Path `
                -Path $env:LOCALAPPDATA `
                -ChildPath 'Microsoft\Edge\User Data\Default\Bookmarks'

            $rootFolderName = 'Edge'

            # get Edge bookmarks
            $bookmarks = Get-ChromiumBookmarks `
                -BookmarksFilePath $bookmarksFilePath `
                -RootFolderName $rootFolderName `
                -BrowserName $browser.Name

        }
        elseif ($Chrome) {
            # validate Chrome installation
            $browser = $Script:installedBrowsers | Microsoft.PowerShell.Core\Where-Object { $PSItem.Name -like '*Chrome*' }
            if (-not $browser) {
                Microsoft.PowerShell.Utility\Write-Host 'Google Chrome is not installed.'
                return
            }
            # construct path to Chrome bookmarks file
            $bookmarksFilePath = Microsoft.PowerShell.Management\Join-Path -Path "${env:LOCALAPPDATA}" -ChildPath 'Google\Chrome\User Data\Default\Bookmarks'
            $rootFolderName = 'Chrome'
            # get Chrome bookmarks
            $bookmarks = Get-ChromiumBookmarks -bookmarksFilePath $bookmarksFilePath -rootFolderName $rootFolderName -browserName ($browser.Name)
        }
        elseif ($Firefox) {
            # validate Firefox installation
            $browser = $Script:installedBrowsers | Microsoft.PowerShell.Core\Where-Object { $PSItem.Name -like '*Firefox*' }
            if (-not $browser) {
                Microsoft.PowerShell.Utility\Write-Host 'Mozilla Firefox is not installed.'
                return
            }
            # find Firefox profile folder
            $profileFolderPath = "$env:APPDATA\Mozilla\Firefox\Profiles"
            $profileFolder = Microsoft.PowerShell.Management\Get-ChildItem -LiteralPath  $profileFolderPath -Directory | Microsoft.PowerShell.Core\Where-Object { $PSItem.Name -match '\.default-release$' } | Microsoft.PowerShell.Utility\Select-Object -First 1
            if ($null -eq $profileFolder) {
                Microsoft.PowerShell.Utility\Write-Host 'Firefox profile folder not found.'
                return
            }
            # construct path to Firefox places.sqlite file
            $placesFilePath = Microsoft.PowerShell.Management\Join-Path -Path $profileFolder.FullName -ChildPath 'places.sqlite'
            # get Firefox bookmarks
            $bookmarks = Get-FirefoxBookmark -placesFilePath $placesFilePath -browserName ($browser.Name)
        }
        else {
            Microsoft.PowerShell.Utility\Write-Warning 'Please specify either -Chrome, -Edge, or -Firefox switch.'
            return
        }

        return $bookmarks
    }

    end {
    }
}