diff --git a/Core/ExtensionManager.cs b/Core/ExtensionManager.cs index ef4fb71..61441e8 100644 --- a/Core/ExtensionManager.cs +++ b/Core/ExtensionManager.cs @@ -19,9 +19,9 @@ public sealed class PinnedExtensionItem public sealed class ExtensionManager { private readonly CoreWebView2Profile _profile; - private readonly string _metadataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FoxBrowser", "extension-options.json"); - private readonly string _uiMetadataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FoxBrowser", "extension-ui.json"); - private readonly string _pinnedPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FoxBrowser", "pinned-extensions.json"); + private readonly string _metadataPath = Path.Combine(AppContext.BaseDirectory, "extension-options.json"); + private readonly string _uiMetadataPath = Path.Combine(AppContext.BaseDirectory, "extension-ui.json"); + private readonly string _pinnedPath = Path.Combine(AppContext.BaseDirectory, "pinned-extensions.json"); private readonly Dictionary _optionsPages; private readonly Dictionary _uiMetadata; private readonly HashSet _pinnedIds; diff --git a/Core/SessionStore.cs b/Core/SessionStore.cs index 55f45a8..c93a0d3 100644 --- a/Core/SessionStore.cs +++ b/Core/SessionStore.cs @@ -30,16 +30,14 @@ public sealed class Session /// /// Saves and loads the browser session (open tabs + per-tab state) to a JSON -/// file in LocalApplicationData\FoxBrowser\session.json. +/// file in the portable FoxBrowser folder next to the executable. /// /// Crash-recovery strategy: the file is re-written after every successful /// NavigationCompleted so a hard crash never loses more than one page load. /// public static class SessionStore { - private static readonly string SessionPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "FoxBrowser", "session.json"); + private static readonly string SessionPath = Path.Combine(AppContext.BaseDirectory, "session.json"); private static readonly JsonSerializerOptions JsonOptions = new() { diff --git a/Core/VpnManager.cs b/Core/VpnManager.cs index 509d656..5da8b4b 100644 --- a/Core/VpnManager.cs +++ b/Core/VpnManager.cs @@ -27,9 +27,7 @@ public sealed class VpnProfile public sealed class VpnManager { - private readonly string _profileDirectory = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "FoxBrowser", "VPN"); + private readonly string _profileDirectory = Path.Combine(AppContext.BaseDirectory, "VPN"); private string? _activeTunnel; public VpnManager() diff --git a/Core/WebView2EnvironmentFactory.cs b/Core/WebView2EnvironmentFactory.cs index b4ad3d3..112a2fa 100644 --- a/Core/WebView2EnvironmentFactory.cs +++ b/Core/WebView2EnvironmentFactory.cs @@ -1,41 +1,28 @@ 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. +/// Creates WebView2 using only the portable profile next to FoxBrowser.exe. +/// No AppData/LocalAppData fallback is used. /// 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. + /// Kept for startup compatibility. Portable mode never switches to a user profile. /// + public string? TakeStartupNotice() => null; + public async Task CreateAsync(string? userDataFolderOverride = null) { - var userDataFolder = userDataFolderOverride ?? SelectPersistentProfile(); + var userDataFolder = userDataFolderOverride ?? PortableProfilePath; Directory.CreateDirectory(userDataFolder); return await CoreWebView2Environment.CreateAsync( @@ -44,179 +31,8 @@ public sealed class WebView2EnvironmentFactory 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. - } - } } diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index cd7a774..02558ac 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -54,7 +54,7 @@ public partial class MainWindow : Window // Use a disposable recovery profile so the browser can still start. recoveryMode = true; var recoveryData = Path.Combine( - Path.GetTempPath(), $"FoxBrowser_Recovery_{Environment.ProcessId}_{Guid.NewGuid():N}"); + AppContext.BaseDirectory, $"Profile_Recovery_{Environment.ProcessId}_{Guid.NewGuid():N}"); Directory.CreateDirectory(recoveryData); environment = await environmentFactory.CreateAsync(recoveryData); } diff --git a/UI/ExtensionsSettingsWindow.xaml b/UI/ExtensionsSettingsWindow.xaml index 52b82c8..afda7fb 100644 --- a/UI/ExtensionsSettingsWindow.xaml +++ b/UI/ExtensionsSettingsWindow.xaml @@ -15,7 +15,7 @@ - + diff --git a/UI/ExtensionsSettingsWindow.xaml.cs b/UI/ExtensionsSettingsWindow.xaml.cs index 8eeab0f..8105ff1 100644 --- a/UI/ExtensionsSettingsWindow.xaml.cs +++ b/UI/ExtensionsSettingsWindow.xaml.cs @@ -33,7 +33,7 @@ public partial class ExtensionsSettingsWindow : Window Loaded += async (_, _) => { await _manager.RefreshAsync(); - Dispatcher.BeginInvoke(RefreshPinCheckboxes); + Dispatcher.Invoke(RefreshPinCheckboxes); }; } @@ -45,7 +45,7 @@ public partial class ExtensionsSettingsWindow : Window try { await _manager.AddExtensionAsync(dialog.SelectedPath); - Dispatcher.BeginInvoke(RefreshPinCheckboxes); + Dispatcher.Invoke(RefreshPinCheckboxes); } catch (Exception ex) { ShowError(ex.Message); } } @@ -64,7 +64,7 @@ public partial class ExtensionsSettingsWindow : Window { await _manager.AddArchiveAsync(dialog.FileName); ExtensionsStatus.Text = "Розширення розпаковано у профіль FoxBrowser та підключено."; - Dispatcher.BeginInvoke(RefreshPinCheckboxes); + Dispatcher.Invoke(RefreshPinCheckboxes); } catch (Exception ex) { ShowError($"Не вдалося імпортувати архів: {ex.Message}"); } }