mirror of
https://github.com/ChrisTitusTech/winutil
synced 2026-06-04 14:17:27 +00:00
Refactor: Secure literal search and defensive scope handling (#4492)
* Refactor: Secure literal search and defensive scope handling * Refactor: Secure literal search for Tweaks Tab * Merge branch 'ChrisTitusTech:main' into fix-search-security
This commit is contained in:
@@ -3,13 +3,41 @@ function Find-AppsByNameOrDescription {
|
||||
.SYNOPSIS
|
||||
Searches through the Apps on the Install Tab and hides all entries that do not match the string
|
||||
|
||||
.DESCRIPTION
|
||||
Filters application entries by name or description using literal string matching.
|
||||
Respects collapsed category state and handles null $sync gracefully.
|
||||
|
||||
.PARAMETER SearchString
|
||||
The string to be searched for
|
||||
The string to be searched for. Wildcards are treated as literal characters.
|
||||
|
||||
.NOTES
|
||||
- Uses module-scope $sync (no parameter needed; inherits from caller's scope)
|
||||
- Performs literal matching (no wildcard expansion)
|
||||
- Safely handles missing hashtable keys and null UI elements
|
||||
- Protected by try/catch to prevent UI thread crashes
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$SearchString = ""
|
||||
)
|
||||
|
||||
# Validate that $sync exists and has required structure
|
||||
if ($null -eq $sync) {
|
||||
Write-Warning "Find-AppsByNameOrDescription: Global `$sync not found. Aborting search."
|
||||
return
|
||||
}
|
||||
|
||||
if ($null -eq $sync.ItemsControl) {
|
||||
Write-Warning "Find-AppsByNameOrDescription: `$sync.ItemsControl not initialized. Aborting search."
|
||||
return
|
||||
}
|
||||
|
||||
if ($null -eq $sync.configs -or $null -eq $sync.configs.applicationsHashtable) {
|
||||
Write-Warning "Find-AppsByNameOrDescription: `$sync.configs.applicationsHashtable not initialized. Aborting search."
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Reset the visibility if the search string is empty or the search is cleared
|
||||
if ([string]::IsNullOrWhiteSpace($SearchString)) {
|
||||
$sync.ItemsControl.Items | ForEach-Object {
|
||||
@@ -26,7 +54,8 @@ function Find-AppsByNameOrDescription {
|
||||
# Respect the collapsed state of categories (indicated by + prefix)
|
||||
if ($categoryLabel.Content -like "+*") {
|
||||
$wrapPanel.Visibility = [Windows.Visibility]::Collapsed
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
$wrapPanel.Visibility = [Windows.Visibility]::Visible
|
||||
}
|
||||
|
||||
@@ -39,6 +68,9 @@ function Find-AppsByNameOrDescription {
|
||||
return
|
||||
}
|
||||
|
||||
# Escape wildcard characters for literal matching
|
||||
$escapedSearchString = [System.Management.Automation.WildcardPattern]::Escape($SearchString)
|
||||
|
||||
# Perform search
|
||||
$sync.ItemsControl.Items | ForEach-Object {
|
||||
# Each item is a StackPanel container with Children[0] = label, Children[1] = WrapPanel
|
||||
@@ -52,8 +84,20 @@ function Find-AppsByNameOrDescription {
|
||||
|
||||
# Search through apps in this category
|
||||
$wrapPanel.Children | ForEach-Object {
|
||||
$appEntry = $sync.configs.applicationsHashtable.$($_.Tag)
|
||||
if ($appEntry.Content -like "*$SearchString*" -or $appEntry.Description -like "*$SearchString*") {
|
||||
# Safely retrieve app entry from hashtable
|
||||
$appTag = $_.Tag
|
||||
$appEntry = $null
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($appTag) -and $sync.configs.applicationsHashtable.ContainsKey($appTag)) {
|
||||
$appEntry = $sync.configs.applicationsHashtable[$appTag]
|
||||
}
|
||||
|
||||
# Check if app matches search criteria
|
||||
if ($null -ne $appEntry) {
|
||||
$contentMatch = $appEntry.Content -like "*$escapedSearchString*"
|
||||
$descriptionMatch = $appEntry.Description -like "*$escapedSearchString*"
|
||||
|
||||
if ($contentMatch -or $descriptionMatch) {
|
||||
# Show the App and mark that this category has a match
|
||||
$_.Visibility = [Windows.Visibility]::Visible
|
||||
$categoryHasMatch = $true
|
||||
@@ -62,6 +106,11 @@ function Find-AppsByNameOrDescription {
|
||||
$_.Visibility = [Windows.Visibility]::Collapsed
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Hide app if no entry found (data integrity issue)
|
||||
$_.Visibility = [Windows.Visibility]::Collapsed
|
||||
}
|
||||
}
|
||||
|
||||
# If category has matches, show the WrapPanel and update the category label to expanded state
|
||||
if ($categoryHasMatch) {
|
||||
@@ -71,10 +120,17 @@ function Find-AppsByNameOrDescription {
|
||||
if ($categoryLabel.Content -like "+*") {
|
||||
$categoryLabel.Content = $categoryLabel.Content -replace "^\+ ", "- "
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
# Hide the entire category container if no matches
|
||||
$_.Visibility = [Windows.Visibility]::Collapsed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Find-AppsByNameOrDescription: An error occurred during search: $_"
|
||||
# Fail gracefully - do not crash the UI thread
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,98 +3,300 @@ function Find-TweaksByNameOrDescription {
|
||||
.SYNOPSIS
|
||||
Searches through the Tweaks on the Tweaks Tab and hides all entries that do not match the search string
|
||||
|
||||
.DESCRIPTION
|
||||
Filters tweak entries by name or description using literal string matching (no wildcard expansion).
|
||||
Respects collapsed category state and handles null $sync gracefully.
|
||||
Safe for rapid keystroke events; no terminal spam on error conditions.
|
||||
|
||||
.PARAMETER SearchString
|
||||
The string to be searched for
|
||||
The string to be searched for. Wildcards are treated as literal characters.
|
||||
|
||||
.NOTES
|
||||
- Uses module-scope $sync (resolved via global/script fallback if needed)
|
||||
- Performs literal matching (no wildcard expansion)
|
||||
- Safely handles missing UI elements and null properties
|
||||
- Protected by try/catch to prevent UI thread crashes
|
||||
- PowerShell 5.1 compatible (no ternary operators, no advanced language features)
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$SearchString = ""
|
||||
)
|
||||
|
||||
# Reset the visibility if the search string is empty or the search is cleared
|
||||
if ([string]::IsNullOrWhiteSpace($SearchString)) {
|
||||
# Show all categories
|
||||
$tweakspanel = $sync.Form.FindName("tweakspanel")
|
||||
$tweakspanel.Children | ForEach-Object {
|
||||
$_.Visibility = [Windows.Visibility]::Visible
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# 1. RESOLVE $SYNC WITH MULTI-LEVEL FALLBACK
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Foreach category section, show all items
|
||||
if ($_ -is [Windows.Controls.Border]) {
|
||||
$_.Visibility = [Windows.Visibility]::Visible
|
||||
if ($null -eq $Sync) {
|
||||
$Sync = $global:sync
|
||||
if ($null -eq $Sync) {
|
||||
$Sync = $script:sync
|
||||
}
|
||||
}
|
||||
|
||||
# Find ItemsControl
|
||||
$dockPanel = $_.Child
|
||||
if ($dockPanel -is [Windows.Controls.DockPanel]) {
|
||||
$itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] }
|
||||
if ($itemsControl) {
|
||||
# Show items in the category
|
||||
foreach ($item in $itemsControl.Items) {
|
||||
if ($item -is [Windows.Controls.Label]) {
|
||||
$item.Visibility = [Windows.Visibility]::Visible
|
||||
} elseif ($item -is [Windows.Controls.DockPanel] -or
|
||||
$item -is [Windows.Controls.StackPanel]) {
|
||||
$item.Visibility = [Windows.Visibility]::Visible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# Validate that $Sync exists and has required structure
|
||||
if ($null -eq $Sync) {
|
||||
# Silent return - function called on every keystroke; no warning spam
|
||||
return
|
||||
}
|
||||
|
||||
# Search for matching tweaks when search string is not null
|
||||
$tweakspanel = $sync.Form.FindName("tweakspanel")
|
||||
if ($null -eq $Sync.Form) {
|
||||
# Silent return - form not yet initialized
|
||||
return
|
||||
}
|
||||
|
||||
$tweakspanel.Children | ForEach-Object {
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# 2. GET REFERENCE TO TWEAKS PANEL
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
$tweaksPanel = $null
|
||||
try {
|
||||
$tweaksPanel = $Sync.Form.FindName("tweakspanel")
|
||||
}
|
||||
catch {
|
||||
# Silent return - panel not found or disposed
|
||||
return
|
||||
}
|
||||
|
||||
if ($null -eq $tweaksPanel) {
|
||||
# Silent return - panel doesn't exist
|
||||
return
|
||||
}
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# 3. HANDLE EMPTY/WHITESPACE SEARCH STRING - RESET TO DEFAULT STATE
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($SearchString)) {
|
||||
try {
|
||||
$tweaksPanel.Children | ForEach-Object {
|
||||
$categoryBorder = $_
|
||||
$categoryVisible = $false
|
||||
|
||||
if ($_ -is [Windows.Controls.Border]) {
|
||||
# Find the ItemsControl
|
||||
$dockPanel = $_.Child
|
||||
# Safely set visibility
|
||||
if ($null -ne $categoryBorder) {
|
||||
$categoryBorder.Visibility = [Windows.Visibility]::Visible
|
||||
}
|
||||
|
||||
# Process each category
|
||||
if ($categoryBorder -is [Windows.Controls.Border]) {
|
||||
$dockPanel = $null
|
||||
if ($null -ne $categoryBorder.Child) {
|
||||
$dockPanel = $categoryBorder.Child
|
||||
}
|
||||
|
||||
if ($dockPanel -is [Windows.Controls.DockPanel]) {
|
||||
$itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] }
|
||||
if ($itemsControl) {
|
||||
$itemsControl = $null
|
||||
$itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] } | Select-Object -First 1
|
||||
|
||||
if ($null -ne $itemsControl) {
|
||||
# Show all items in the category
|
||||
foreach ($item in $itemsControl.Items) {
|
||||
if ($null -ne $item) {
|
||||
# Check if it's a category label (first Label in the ItemsControl)
|
||||
if ($item -is [Windows.Controls.Label]) {
|
||||
$item.Visibility = [Windows.Visibility]::Visible
|
||||
}
|
||||
elseif ($item -is [Windows.Controls.DockPanel] -or $item -is [Windows.Controls.StackPanel]) {
|
||||
# Show all checkbox containers
|
||||
$item.Visibility = [Windows.Visibility]::Visible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Silent catch - UI element may be disposed
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# 4. PERFORM LITERAL SEARCH (NO WILDCARD EXPANSION)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
try {
|
||||
# Normalize search term once for the entire operation
|
||||
$searchTerm = $SearchString
|
||||
if ($null -eq $searchTerm) {
|
||||
$searchTerm = ""
|
||||
}
|
||||
|
||||
# Iterate through all categories
|
||||
$tweaksPanel.Children | ForEach-Object {
|
||||
$categoryBorder = $_
|
||||
$categoryHasMatch = $false
|
||||
|
||||
if ($categoryBorder -is [Windows.Controls.Border]) {
|
||||
$dockPanel = $null
|
||||
if ($null -ne $categoryBorder.Child) {
|
||||
$dockPanel = $categoryBorder.Child
|
||||
}
|
||||
|
||||
if ($dockPanel -is [Windows.Controls.DockPanel]) {
|
||||
$itemsControl = $null
|
||||
$itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] } | Select-Object -First 1
|
||||
|
||||
if ($null -ne $itemsControl) {
|
||||
$categoryLabel = $null
|
||||
|
||||
# Process all items in the ItemsControl
|
||||
# Process all items (checkboxes, labels, panels) in the ItemsControl
|
||||
for ($i = 0; $i -lt $itemsControl.Items.Count; $i++) {
|
||||
$item = $itemsControl.Items[$i]
|
||||
|
||||
if ($null -eq $item) {
|
||||
continue
|
||||
}
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Check if this is a category label (usually first Label)
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
if ($item -is [Windows.Controls.Label]) {
|
||||
$categoryLabel = $item
|
||||
# Initially hide category label; show it only if matches found
|
||||
$item.Visibility = [Windows.Visibility]::Collapsed
|
||||
} elseif ($item -is [Windows.Controls.DockPanel]) {
|
||||
}
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Check if this is a DockPanel containing a tweak checkbox
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
elseif ($item -is [Windows.Controls.DockPanel]) {
|
||||
$checkbox = $null
|
||||
$label = $null
|
||||
|
||||
# Safely extract checkbox and label
|
||||
$checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1
|
||||
$label = $item.Children | Where-Object { $_ -is [Windows.Controls.Label] } | Select-Object -First 1
|
||||
|
||||
if ($label -and ($label.Content -like "*$SearchString*" -or $label.ToolTip -like "*$SearchString*")) {
|
||||
# Check if tweak matches search criteria
|
||||
$itemMatches = $false
|
||||
|
||||
if ($null -ne $label) {
|
||||
$labelContent = $label.Content
|
||||
$labelToolTip = $label.ToolTip
|
||||
|
||||
# Safely null-check properties
|
||||
if ($null -eq $labelContent) {
|
||||
$labelContent = ""
|
||||
}
|
||||
if ($null -eq $labelToolTip) {
|
||||
$labelToolTip = ""
|
||||
}
|
||||
|
||||
# Convert to string and perform LITERAL matching
|
||||
$labelContentStr = [string]$labelContent
|
||||
$labelToolTipStr = [string]$labelToolTip
|
||||
|
||||
# Use IndexOf for literal matching (no wildcard interpretation)
|
||||
$contentMatch = $labelContentStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0
|
||||
$toolTipMatch = $labelToolTipStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0
|
||||
|
||||
if ($contentMatch -or $toolTipMatch) {
|
||||
$itemMatches = $true
|
||||
}
|
||||
}
|
||||
|
||||
# Set visibility based on match result
|
||||
if ($itemMatches) {
|
||||
$item.Visibility = [Windows.Visibility]::Visible
|
||||
if ($categoryLabel) { $categoryLabel.Visibility = [Windows.Visibility]::Visible }
|
||||
$categoryVisible = $true
|
||||
} else {
|
||||
$categoryHasMatch = $true
|
||||
}
|
||||
else {
|
||||
$item.Visibility = [Windows.Visibility]::Collapsed
|
||||
}
|
||||
} elseif ($item -is [Windows.Controls.StackPanel]) {
|
||||
# StackPanel which contain checkboxes or other elements
|
||||
}
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Check if this is a StackPanel containing a tweak checkbox
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
elseif ($item -is [Windows.Controls.StackPanel]) {
|
||||
$checkbox = $null
|
||||
$checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1
|
||||
|
||||
if ($checkbox -and ($checkbox.Content -like "*$SearchString*" -or $checkbox.ToolTip -like "*$SearchString*")) {
|
||||
$itemMatches = $false
|
||||
|
||||
if ($null -ne $checkbox) {
|
||||
$checkboxContent = $checkbox.Content
|
||||
$checkboxToolTip = $checkbox.ToolTip
|
||||
|
||||
# Safely null-check properties
|
||||
if ($null -eq $checkboxContent) {
|
||||
$checkboxContent = ""
|
||||
}
|
||||
if ($null -eq $checkboxToolTip) {
|
||||
$checkboxToolTip = ""
|
||||
}
|
||||
|
||||
# Convert to string and perform LITERAL matching
|
||||
$checkboxContentStr = [string]$checkboxContent
|
||||
$checkboxToolTipStr = [string]$checkboxToolTip
|
||||
|
||||
# Use IndexOf for literal matching (no wildcard interpretation)
|
||||
$contentMatch = $checkboxContentStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0
|
||||
$toolTipMatch = $checkboxToolTipStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0
|
||||
|
||||
if ($contentMatch -or $toolTipMatch) {
|
||||
$itemMatches = $true
|
||||
}
|
||||
}
|
||||
|
||||
# Set visibility based on match result
|
||||
if ($itemMatches) {
|
||||
$item.Visibility = [Windows.Visibility]::Visible
|
||||
if ($categoryLabel) { $categoryLabel.Visibility = [Windows.Visibility]::Visible }
|
||||
$categoryVisible = $true
|
||||
} else {
|
||||
$categoryHasMatch = $true
|
||||
}
|
||||
else {
|
||||
$item.Visibility = [Windows.Visibility]::Collapsed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Update category label visibility and expanded/collapsed state
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
if ($categoryHasMatch) {
|
||||
# Show category label
|
||||
if ($null -ne $categoryLabel) {
|
||||
$categoryLabel.Visibility = [Windows.Visibility]::Visible
|
||||
|
||||
# Update category label to expanded state (change "+" to "-")
|
||||
$labelContent = $categoryLabel.Content
|
||||
if ($null -ne $labelContent) {
|
||||
$labelStr = [string]$labelContent
|
||||
|
||||
# Safe string replacement without -replace regex
|
||||
if ($labelStr.StartsWith("+ ")) {
|
||||
$expandedLabel = "- " + $labelStr.Substring(2)
|
||||
$categoryLabel.Content = $expandedLabel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Set the visibility based on if any item matched
|
||||
$categoryBorder.Visibility = if ($categoryVisible) { [Windows.Visibility]::Visible } else { [Windows.Visibility]::Collapsed }
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
# Set category border visibility based on whether it has matches
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
|
||||
if ($categoryHasMatch) {
|
||||
$categoryBorder.Visibility = [Windows.Visibility]::Visible
|
||||
}
|
||||
else {
|
||||
$categoryBorder.Visibility = [Windows.Visibility]::Collapsed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Silent catch - UI elements may be disposed or in unexpected state
|
||||
# Do not log to terminal as this function is called on every keystroke
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user