GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
FirefoxProfileManager.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.IO;
20using System.Linq;
21using System.Text;
22using System.Text.RegularExpressions;
23using System.Threading.Tasks;
24using static GenerallyPositive.Enums;
25
27{
28 internal class FirefoxProfileManager
29 {
30 // generated by grok
31 // to replace these geckodriver options
32 /*
33 // PDF handling
34 if (browserSettings.OpenPDFExternally)
35 firefoxArguments.Add("--pdfjs.disabled");
36
37 // Download settings
38 if (!browserSettings.PromptForDownload)
39 firefoxArguments.Add($"--browser.helperApps.neverAsk.saveToDisk={GPAL.GPALSettings.FirefoxDirectDownloadMimeTypes}");
40
41 if (!browserSettings.LoadImages)
42 firefoxArguments.Add("--permissions.default.image=0");
43 else
44 firefoxArguments.Add("--permissions.default.image=1");
45
46 if (!browserSettings.BlockPopUps)
47 firefoxArguments.Add("--dom.popup_maximum=0");
48 else
49 firefoxArguments.Add("--dom.popup_maximum=20");
50
51 firefoxArguments.Add("--safebrowsing.enabled=false");
52
53 if (!string.IsNullOrEmpty(browserSettings.DownloadLocation))
54 {
55 firefoxArguments.Add($"--browser.download.dir={browserSettings.DownloadLocation}");
56 firefoxArguments.Add("--browser.download.folderList=2");
57 }
58 */
59 // the preferences GPAL sets from the workflow, so apply and restore cannot drift apart. firefox has no
60 // json preferences file and no CDP, so these are the only way to say anything to it about downloads
61 private static readonly string[] userJsPreferenceKeys =
62 {
63 "pdfjs.disabled",
64 "browser.download.useDownloadDir",
65 "browser.helperApps.neverAsk.saveToDisk",
66 "browser.download.dir",
67 "browser.download.folderList",
68 "permissions.default.image",
69 "dom.popup_maximum",
70 "browser.safebrowsing.enabled",
71 "extensions.enabled",
72 "xpinstall.signatures.required",
73 "dom.webdriver.enabled",
74 "browser.startup.page",
75 "browser.sessionstore.resume_from_crash",
76 };
77
84 private static Dictionary<string, string> ReadUserJs(string userJsPath)
85 {
86 Dictionary<string, string> retVal = new Dictionary<string, string>();
87
88 if (true == File.Exists(userJsPath))
89 foreach (string line in File.ReadAllLines(userJsPath))
90 {
91 Match match = Regex.Match(line, @"user_pref\‍(""([^""]+)"",\s*(.+)\‍);");
92
93 if (true == match.Success)
94 retVal[match.Groups[1].Value] = match.Groups[2].Value.Trim();
95 }
96
97 return retVal;
98 }
99
106 private static void WriteUserJs(string userJsPath, Dictionary<string, string> preferences)
107 {
108 StringBuilder builder = new StringBuilder();
109
110 foreach (KeyValuePair<string, string> preference in preferences.OrderBy(p => p.Key, StringComparer.Ordinal))
111 builder.AppendLine($"user_pref(\"{preference.Key}\", {preference.Value});");
112
113 File.WriteAllText(userJsPath, builder.ToString());
114 }
115
121 private static string AsJsString(string value)
122 {
123 string retVal = $"\"{value?.Replace("\\", "\\\\").Replace("\"", "\\\"")}\"";
124
125 return retVal;
126 }
127
139 public static Dictionary<string, string> CreateUserJs(string profilePath, BrowserSettings browserSettings)
140 {
141 Dictionary<string, string> retVal = null;
142 string userJsPath = Path.Combine(profilePath, "user.js");
143
144 try
145 {
146 Dictionary<string, string> preferences = ReadUserJs(userJsPath);
147
148 retVal = new Dictionary<string, string>();
149 foreach (string key in userJsPreferenceKeys)
150 retVal[key] = preferences.ContainsKey(key) ? preferences[key] : null;
151
152 // pdfjs is firefox's built in viewer. left on, a pdf is rendered and no download ever starts, so
153 // there is nothing for the download watcher to see.
154 // headless turns it off whatever the workflow asked, because there is nobody to render it for and
155 // Get() sets OpenPDFExternally false for headless, which would otherwise switch the viewer back
156 // on and leave every pdf displayed to no one instead of downloaded. chrome does not need saying
157 // because a headless chrome cannot display a pdf and downloads it; firefox's viewer works headless
158 preferences["pdfjs.disabled"] = browserSettings.OpenPDFExternally || browserSettings.UseHeadless ? "true" : "false";
159
160 // false is firefox for "ask where to save every file", which is the save dialog
161 preferences["browser.download.useDownloadDir"] = browserSettings.PromptForDownload ? "false" : "true";
162 preferences["browser.helperApps.neverAsk.saveToDisk"] = true == browserSettings.PromptForDownload
163 ? AsJsString("")
164 : AsJsString(GPAL.GPALSettings.FirefoxDirectDownloadMimeTypes);
165
166 preferences["permissions.default.image"] = browserSettings.LoadImages ? "1" : "0";
167 // how many popups one event may open. zero is none, and twenty is what firefox ships with,
168 // so blocking is the low number and the sense of this reads backwards
169 preferences["dom.popup_maximum"] = browserSettings.BlockPopUps ? "0" : "20";
170 preferences["browser.safebrowsing.enabled"] = "false";
171
172 // 0 is a blank start. a workflow starts against the page it navigates to, not against whatever
173 // was on screen when the browser last closed, and not against a crash restore prompt either.
174 // geckodriver happens to leave these behind on a profile it has driven, which is not the same as
175 // them being set
176 preferences["browser.startup.page"] = "0";
177 preferences["browser.sessionstore.resume_from_crash"] = "false";
178
179 // enable and allow unsigned extensions - we will load magic off disk
180 preferences["extensions.enabled"] = "true";
181 preferences["xpinstall.signatures.required"] = "false";
182 preferences["dom.webdriver.enabled"] = "false";
183
184 if (false == string.IsNullOrEmpty(browserSettings.DownloadLocation))
185 {
186 preferences["browser.download.dir"] = AsJsString(browserSettings.DownloadLocation);
187 preferences["browser.download.folderList"] = "2"; // 2: use the custom download directory
188 }
189 else
190 {
191 // nothing was asked for, so the profile's own download directory is the right one and a
192 // directory left behind by an earlier run is not
193 preferences.Remove("browser.download.dir");
194 preferences.Remove("browser.download.folderList");
195 }
196
197 WriteUserJs(userJsPath, preferences);
198 }
199 catch (Exception ex)
200 {
201 retVal = null;
202 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Could not write preferences into [{userJsPath}]", null, GPALObjectType.None, ex);
203 }
204
205 return retVal;
206 }
207
215 public static void RestoreUserJs(string profilePath, Dictionary<string, string> previous)
216 {
217 string userJsPath = null == previous ? null : Path.Combine(profilePath, "user.js");
218
219 if (null == userJsPath || false == File.Exists(userJsPath))
220 return;
221
222 try
223 {
224 Dictionary<string, string> preferences = ReadUserJs(userJsPath);
225
226 foreach (KeyValuePair<string, string> was in previous)
227 if (null == was.Value)
228 preferences.Remove(was.Key); // it was not there before, so it goes back to not being there
229 else
230 preferences[was.Key] = was.Value;
231
232 WriteUserJs(userJsPath, preferences);
233 }
234 catch (Exception ex)
235 {
236 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Could not restore preferences in [{userJsPath}]", null, GPALObjectType.None, ex);
237 }
238 }
239
244 public static string CreateTempProfileDirectory()
245 {
246 // Get the system's %TEMP% directory
247 string tempPath = Path.GetTempPath();
248
249 // Generate a random string (e.g., 8 characters)
250 string randomString = Guid.NewGuid().ToString("N").Substring(0, 8);
251
252 // Create the directory name (e.g., rust_mozprofileXYZ123)
253 string profileDirName = $"rust_mozprofile{randomString}";
254
255 // Combine with %TEMP% to get the full path
256 string profilePath = Path.Combine(tempPath, profileDirName);
257
258 // Create the directory if it doesn't exist
259 Directory.CreateDirectory(profilePath);
260
261 return profilePath;
262 }
263 }
264}
265