Skip to content
This repository was archived by the owner on Mar 21, 2021. It is now read-only.

Commit 94b4e11

Browse files
committed
Fixed issue where browsers are missing "ApplicationName" values in the registry, such as Internet Explorer, weren't recognised.
1 parent 5655c2c commit 94b4e11

File tree

7 files changed

+147
-36
lines changed

7 files changed

+147
-36
lines changed

SetDefaultBrowser/ApplicationCapabilities.cs

Lines changed: 0 additions & 16 deletions
This file was deleted.

SetDefaultBrowser/Browser.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,20 @@
22
{
33
public class Browser
44
{
5+
/// <summary>
6+
/// Uniquely identifies the browser for Windows' "Set Default Programs" applet.
7+
/// Not suitable for display to user.
8+
/// </summary>
59
public string UniqueApplicationName { get; set; }
6-
public ApplicationCapabilities Capabilities { get; set; }
710

8-
public override string ToString() => Capabilities.DisplayName;
11+
public string DisplayName { get; set; }
12+
13+
/// <summary>
14+
/// File extensions are in the form ".abc".
15+
/// Protocols are in the form "abc".
16+
/// </summary>
17+
public string[] Associations { get; set; }
18+
19+
public override string ToString() => DisplayName;
920
}
1021
}

SetDefaultBrowser/BrowserRegistry.cs

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Win32;
22
using System;
33
using System.Collections.Generic;
4+
using System.ComponentModel;
45
using System.Linq;
56
using System.Text;
67
using System.Threading.Tasks;
@@ -18,17 +19,20 @@ public static Browser[] GetInstalledBrowsers()
1819
{
1920
var capabilities = TryGetCapabilities(registeredApplication.Value);
2021
if (capabilities != null)
21-
if (
22-
capabilities.DisplayName != null
23-
&&
24-
// These protocols are needed for Windows to see it as a web browser.
25-
new[] { "http", "https" }.All(protocol => capabilities.Associations.Contains(protocol, StringComparer.OrdinalIgnoreCase))
26-
)
27-
results.Add(new Browser
28-
{
29-
UniqueApplicationName = registeredApplication.Key,
30-
Capabilities = capabilities,
31-
});
22+
{
23+
var associations = capabilities.Associations.Select(association => association.Association).ToArray();
24+
if (new[] { "http", "https" }.All(protocol => associations.Contains(protocol, StringComparer.OrdinalIgnoreCase)))
25+
{
26+
var displayName = TryGetDisplayName(capabilities);
27+
if (displayName != null)
28+
results.Add(new Browser
29+
{
30+
UniqueApplicationName = registeredApplication.Key,
31+
DisplayName = displayName,
32+
Associations = associations,
33+
});
34+
}
35+
}
3236
}
3337

3438
return results.ToArray();
@@ -66,18 +70,83 @@ private static ApplicationCapabilities TryGetCapabilities(string applicationCapa
6670
{
6771
var resourceValue = new StringBuilder(4096);
6872
var hresult = WindowsApi.SHLoadIndirectString(rawApplicationName, resourceValue, resourceValue.Capacity, IntPtr.Zero);
69-
result.DisplayName = hresult == 0 ? resourceValue.ToString() : rawApplicationName;
73+
result.ApplicationName = hresult == 0 ? resourceValue.ToString() : rawApplicationName;
7074
}
7175

72-
var associations = new List<string>();
76+
var associations = new List<ApplicationAssociation>();
7377
foreach (var subkeyName in new[] { "FileAssociations", "URLAssociations" })
7478
using (var subkey = key.OpenSubKey(subkeyName))
7579
if (subkey != null)
76-
associations.AddRange(subkey.GetValueNames());
80+
foreach (var association in subkey.GetValueNames())
81+
{
82+
var progId = subkey.GetValue(association) as string;
83+
associations.Add(new ApplicationAssociation
84+
{
85+
Association = association,
86+
ProgId = progId,
87+
});
88+
}
89+
7790
result.Associations = associations.ToArray();
7891
}
7992

8093
return result;
8194
}
95+
96+
private static string TryGetDisplayName(ApplicationCapabilities capabilities)
97+
{
98+
// The display name of a registered application is optional. Internet Explorer doesn't use it, for example.
99+
if (capabilities.ApplicationName != null)
100+
return capabilities.ApplicationName;
101+
102+
foreach (var association in capabilities.Associations)
103+
if (!String.IsNullOrEmpty(association.ProgId))
104+
{
105+
var appName = AssocQueryString(association);
106+
if (appName != null)
107+
return appName;
108+
}
109+
110+
return null;
111+
}
112+
113+
private static string AssocQueryString(ApplicationAssociation association)
114+
{
115+
uint appNameLength = 0;
116+
var result = WindowsApi.AssocQueryString(WindowsApi.AssocF.None, WindowsApi.AssocStr.FriendlyAppName, association.ProgId, null, null, ref appNameLength);
117+
if (new[] { 2147943555, 2147942402 }.Contains(result)) // No application is associated with the specified file for this operation OR The system cannot find the file specified
118+
return null;
119+
120+
if (result != 1)
121+
throw new Win32Exception((int)result);
122+
123+
var buffer = new StringBuilder((int)appNameLength);
124+
var bufferLength = (uint)buffer.Capacity;
125+
result = WindowsApi.AssocQueryString(WindowsApi.AssocF.None, WindowsApi.AssocStr.FriendlyAppName, association.ProgId, null, buffer, ref bufferLength);
126+
if (result != 0)
127+
throw new Win32Exception((int)result);
128+
129+
return buffer.ToString();
130+
}
131+
132+
private class ApplicationCapabilities
133+
{
134+
/// <summary>
135+
/// May be <c>null</c>.
136+
/// </summary>
137+
public string ApplicationName { get; set; }
138+
public ApplicationAssociation[] Associations { get; set; }
139+
}
140+
141+
private class ApplicationAssociation
142+
{
143+
public string Association { get; set; }
144+
145+
/// <summary>
146+
/// May be null
147+
/// </summary>
148+
public string ProgId { get; set; }
149+
}
150+
82151
}
83152
}

SetDefaultBrowser/DefaultBrowserChanger.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public static void Set(Browser browser)
4646
var listView = new ListView(listViewHandle);
4747
var save = Wait(() => FindDescendantBy(window, text: "Save"));
4848

49-
var browserAssociations = browser.Capabilities.Associations
49+
var browserAssociations = browser.Associations
5050
.Intersect(new[] { ".htm", ".html", "HTTP", "HTTPS" }, StringComparer.OrdinalIgnoreCase)
5151
.ToArray();
5252

SetDefaultBrowser/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ static int Main(string[] rawArgs)
2727
throw new EnvironmentException("Didn't find any web browsers installed.");
2828

2929
//Using a current culture string comparison here since we're comparing user input against display text.
30-
var browser = browsers.FirstOrDefault(b => b.Capabilities.DisplayName.Equals(args.BrowserName, StringComparison.CurrentCulture));
30+
var browser = browsers.FirstOrDefault(b => b.DisplayName.Equals(args.BrowserName, StringComparison.CurrentCulture));
3131
if (browser == null)
3232
throw new EnvironmentException($"Didn't find a web browser with the name '{args.BrowserName}'.\n\nPlease use one of the following:\n{FormattedInstalledBrowsers(browsers)}");
3333

@@ -50,7 +50,7 @@ static int Main(string[] rawArgs)
5050
private static string ApplicationTitle => Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyTitleAttribute>().Title;
5151
private static string FormattedInstalledBrowsers(IEnumerable<Browser> browsers)
5252
{
53-
return String.Join("\n", browsers.Select(browser => "• " + browser.Capabilities.DisplayName).Distinct().OrderBy(n => n));
53+
return String.Join("\n", browsers.Select(browser => "• " + browser.DisplayName).Distinct().OrderBy(n => n));
5454
}
5555
}
5656
}

SetDefaultBrowser/SetDefaultBrowser.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
<Reference Include="System.Xml" />
5151
</ItemGroup>
5252
<ItemGroup>
53-
<Compile Include="ApplicationCapabilities.cs" />
5453
<Compile Include="Args.cs" />
5554
<Compile Include="Browser.cs" />
5655
<Compile Include="DefaultBrowserChanger.cs" />

SetDefaultBrowser/WindowsApi.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,5 +204,53 @@ public struct PROCESS_INFORMATION
204204

205205
[DllImport("shlwapi.dll", BestFitMapping = false, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = false, ThrowOnUnmappableChar = true)]
206206
public static extern int SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, int cchOutBuf, IntPtr ppvReserved);
207+
208+
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
209+
public static extern uint AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, ref uint pcchOut);
210+
211+
[Flags]
212+
public enum AssocF : uint
213+
{
214+
None = 0,
215+
Init_NoRemapCLSID = 0x1,
216+
Init_ByExeName = 0x2,
217+
Open_ByExeName = 0x2,
218+
Init_DefaultToStar = 0x4,
219+
Init_DefaultToFolder = 0x8,
220+
NoUserSettings = 0x10,
221+
NoTruncate = 0x20,
222+
Verify = 0x40,
223+
RemapRunDll = 0x80,
224+
NoFixUps = 0x100,
225+
IgnoreBaseClass = 0x200,
226+
Init_IgnoreUnknown = 0x400,
227+
Init_FixedProgId = 0x800,
228+
IsProtocol = 0x1000,
229+
InitForFile = 0x2000,
230+
}
231+
232+
public enum AssocStr
233+
{
234+
Command = 1,
235+
Executable,
236+
FriendlyDocName,
237+
FriendlyAppName,
238+
NoOpen,
239+
ShellNewValue,
240+
DDECommand,
241+
DDEIfExec,
242+
DDEApplication,
243+
DDETopic,
244+
InfoTip,
245+
QuickTip,
246+
TileInfo,
247+
ContentType,
248+
DefaultIcon,
249+
ShellExtension,
250+
DropTarget,
251+
DelegateExecute,
252+
SupportedUriProtocols,
253+
Max,
254+
}
207255
}
208256
}

0 commit comments

Comments
 (0)