// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Text; using System.Threading; using Wox.Plugin.Common.Win32; using Wox.Plugin.Logger; namespace Wox.Plugin.Common { /// /// Contains information (e.g. path to executable, name...) about the default browser. /// public static class DefaultBrowserInfo { private static readonly Lock _updateLock = new Lock(); /// Gets the path to the MS Edge browser executable. public static string MSEdgePath => System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Microsoft\Edge\Application\msedge.exe"); /// Gets the command line pattern of the MS Edge. public const string MSEdgeArgumentsPattern = "--single-argument %1"; public const string MSEdgeName = "Microsoft Edge"; /// Gets the path to default browser's executable. public static string Path { get; private set; } /// Gets since the icon is embedded in the executable. public static string IconPath => Path; /// Gets the user-friendly name of the default browser. public static string Name { get; private set; } /// Gets the command line pattern of the default browser. public static string ArgumentsPattern { get; private set; } public static bool IsDefaultBrowserSet { get => !string.IsNullOrEmpty(Path); } public const long UpdateTimeout = 300; private static long _lastUpdateTickCount = -UpdateTimeout; private static bool _updatedOnce; private static bool _errorLogged; /// /// Updates only if at least more than 300ms has passed since the last update, to avoid multiple calls to . /// (because of multiple plugins calling update at the same time.) /// public static void UpdateIfTimePassed() { long curTickCount = Environment.TickCount64; if (curTickCount - _lastUpdateTickCount >= UpdateTimeout) { _lastUpdateTickCount = curTickCount; Update(); } } /// /// Consider using to avoid updating multiple times. /// (because of multiple plugins calling update at the same time.) /// public static void Update() { lock (_updateLock) { if (!_updatedOnce) { Log.Info("I've tried updating the chosen Web Browser info at least once.", typeof(DefaultBrowserInfo)); _updatedOnce = true; } try { string progId = GetRegistryValue( @"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoiceLatest\ProgId", "ProgId") ?? GetRegistryValue( @"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", "ProgId"); string appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName") ?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName"); if (appName != null) { // Handle indirect strings: if (appName.StartsWith('@')) { appName = GetIndirectString(appName); } appName = appName .Replace("URL", null, StringComparison.OrdinalIgnoreCase) .Replace("HTML", null, StringComparison.OrdinalIgnoreCase) .Replace("Document", null, StringComparison.OrdinalIgnoreCase) .Replace("Web", null, StringComparison.OrdinalIgnoreCase) .TrimEnd(); } Name = appName; string commandPattern = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\shell\open\command", null); if (string.IsNullOrEmpty(commandPattern)) { throw new ArgumentOutOfRangeException( nameof(commandPattern), "Default browser program command is not specified."); } if (commandPattern.StartsWith('@')) { commandPattern = GetIndirectString(commandPattern); } // HACK: for firefox installed through Microsoft store // When installed through Microsoft Firefox the commandPattern does not have // quotes for the path. As the Program Files does have a space // the extracted path would be invalid, here we add the quotes to fix it const string FirefoxExecutableName = "firefox.exe"; if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") && (!commandPattern.StartsWith('\"'))) { var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) + FirefoxExecutableName.Length; commandPattern = commandPattern.Insert(pathEndIndex, "\""); commandPattern = commandPattern.Insert(0, "\""); } if (commandPattern.StartsWith('\"')) { var endQuoteIndex = commandPattern.IndexOf('\"', 1); if (endQuoteIndex != -1) { Path = commandPattern.Substring(1, endQuoteIndex - 1); ArgumentsPattern = commandPattern.Substring(endQuoteIndex + 1).Trim(); } } else { var spaceIndex = commandPattern.IndexOf(' '); if (spaceIndex != -1) { Path = commandPattern.Substring(0, spaceIndex); ArgumentsPattern = commandPattern.Substring(spaceIndex + 1).Trim(); } } // Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App if (!System.IO.Path.Exists(Path) && !Uri.TryCreate(Path, UriKind.Absolute, out _)) { throw new ArgumentException( $"Command validation failed: {commandPattern}", nameof(commandPattern)); } if (string.IsNullOrEmpty(Path)) { throw new ArgumentOutOfRangeException( nameof(Path), "Default browser program path could not be determined."); } } catch (Exception e) { // Fallback to MS Edge Path = MSEdgePath; Name = MSEdgeName; ArgumentsPattern = MSEdgeArgumentsPattern; if (!_errorLogged) { Log.Exception("Exception when retrieving browser path/name. Path and Name are set to use Microsoft Edge.", e, typeof(DefaultBrowserInfo)); _errorLogged = true; } } string GetRegistryValue(string registryLocation, string valueName) { return Microsoft.Win32.Registry.GetValue(registryLocation, valueName, null) as string; } string GetIndirectString(string str) { var stringBuilder = new StringBuilder(128); if (NativeMethods.SHLoadIndirectString( str, stringBuilder, (uint)stringBuilder.Capacity, IntPtr.Zero) == HRESULT.S_OK) { return stringBuilder.ToString(); } throw new ArgumentNullException(nameof(str), "Could not load indirect string."); } } } } }