using System.IO; using System.Security; using Microsoft.Web.WebView2.Core; namespace FoxBrowser.Core; /// /// Creates every WebView2 environment with one consistent set of options and /// chooses the portable profile when the application directory is writable. /// public sealed class WebView2EnvironmentFactory { private const string ProductFolderName = "FoxBrowser"; private const string ProfileFolderName = "Profile"; private const string LegacyProfileFolderName = "WebView2"; private const string ExtensionsFolderName = "Extensions"; private const string LegacyExtensionsFolderName = "Extensions"; private const string ProtectedFolderNoticeMarker = "portable-profile-fallback-notified"; private readonly string _applicationDataRoot = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ProductFolderName); private bool _shouldShowProtectedFolderNotice; public string PortableProfilePath => Path.Combine(AppContext.BaseDirectory, ProfileFolderName); public string LegacyProfilePath => Path.Combine(_applicationDataRoot, LegacyProfileFolderName); public static string PortableExtensionsPath => Path.Combine(AppContext.BaseDirectory, ExtensionsFolderName); public static string LegacyExtensionsPath => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ProductFolderName, LegacyExtensionsFolderName); public bool UsingLegacyProfile { get; private set; } public string? MigrationError { get; private set; } /// /// Creates a WebView2 environment. An override is used only by startup /// recovery, which must always receive its own temporary profile. /// public async Task CreateAsync(string? userDataFolderOverride = null) { var userDataFolder = userDataFolderOverride ?? SelectPersistentProfile(); Directory.CreateDirectory(userDataFolder); return await CoreWebView2Environment.CreateAsync( browserExecutableFolder: null, userDataFolder: userDataFolder, options: CreateWebViewOptions()); } /// /// Returns a startup notice once for the protected-installation fallback, /// and returns a migration error when one occurred during this startup. /// public string? TakeStartupNotice() { if (_shouldShowProtectedFolderNotice) { _shouldShowProtectedFolderNotice = false; return "FoxBrowser встановлено в захищену папку — профіль зберігається " + "у стандартному місці Windows, а не поруч із застосунком. Для " + "портативного профілю перенесіть FoxBrowser у папку з правами запису " + "(наприклад, Робочий стіл або Документи)."; } if (MigrationError is not null) return $"Не вдалося перенести профіль FoxBrowser у папку поруч із застосунком: {MigrationError}\n\n" + "Запуск продовжено зі стандартним профілем Windows."; return null; } private string SelectPersistentProfile() { UsingLegacyProfile = false; MigrationError = null; if (!CanWriteToApplicationDirectory()) { UsingLegacyProfile = true; MarkProtectedFolderNotice(); return LegacyProfilePath; } if (!Directory.Exists(PortableProfilePath) && Directory.Exists(LegacyProfilePath)) { try { CopyDirectoryContents(LegacyProfilePath, PortableProfilePath); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException) { MigrationError = ex.Message; TryDeleteDirectory(PortableProfilePath); UsingLegacyProfile = true; return LegacyProfilePath; } } MigrateLegacyExtensionsIfNeeded(); return PortableProfilePath; } private static CoreWebView2EnvironmentOptions CreateWebViewOptions() => new() { AreBrowserExtensionsEnabled = true }; private static bool CanWriteToApplicationDirectory() { var testFile = Path.Combine(AppContext.BaseDirectory, $".foxbrowser-write-test-{Guid.NewGuid():N}.tmp"); try { using (var stream = new FileStream(testFile, FileMode.CreateNew, FileAccess.Write, FileShare.None)) stream.WriteByte(0); File.Delete(testFile); return true; } catch (UnauthorizedAccessException) { TryDeleteFile(testFile); return false; } catch (IOException) { TryDeleteFile(testFile); return false; } catch (SecurityException) { TryDeleteFile(testFile); return false; } } private void MarkProtectedFolderNotice() { try { Directory.CreateDirectory(_applicationDataRoot); var marker = ProtectedFolderNoticeMarkerPath(); if (!File.Exists(marker)) { File.WriteAllText(marker, DateTimeOffset.UtcNow.ToString("O")); _shouldShowProtectedFolderNotice = true; } } catch { // The fallback profile remains valid even if the notice marker cannot be saved. } } private string ProtectedFolderNoticeMarkerPath() => Path.Combine(_applicationDataRoot, ProtectedFolderNoticeMarker); private static void CopyDirectoryContents(string sourceDirectory, string destinationDirectory) { Directory.CreateDirectory(destinationDirectory); foreach (var directory in Directory.EnumerateDirectories(sourceDirectory, "*", SearchOption.AllDirectories)) { var relativePath = Path.GetRelativePath(sourceDirectory, directory); Directory.CreateDirectory(Path.Combine(destinationDirectory, relativePath)); } foreach (var file in Directory.EnumerateFiles(sourceDirectory, "*", SearchOption.AllDirectories)) { var relativePath = Path.GetRelativePath(sourceDirectory, file); var destinationFile = Path.Combine(destinationDirectory, relativePath); Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!); File.Copy(file, destinationFile, overwrite: false); } } private static void MigrateLegacyExtensionsIfNeeded() { if (!Directory.Exists(LegacyExtensionsPath) || !Directory.EnumerateFileSystemEntries(LegacyExtensionsPath).Any()) return; if (Directory.Exists(PortableExtensionsPath) && Directory.EnumerateFileSystemEntries(PortableExtensionsPath).Any()) return; try { CopyDirectoryContents(LegacyExtensionsPath, PortableExtensionsPath); } catch (IOException) { // Keep the legacy folder intact. A later startup can retry the migration. } catch (UnauthorizedAccessException) { // Keep the legacy folder intact and let the normal profile startup continue. } catch (SecurityException) { // Keep the legacy folder intact and let the normal profile startup continue. } } private static void TryDeleteDirectory(string path) { try { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); } catch { // A partial migration must not prevent fallback to the legacy profile. } } private static void TryDeleteFile(string path) { try { if (File.Exists(path)) File.Delete(path); } catch { // Best effort cleanup only. } } }