30 public class SeleniumStorageHelper
32 internal IWebDriver webDriver;
33 internal ChromiumDriver browserDriver;
35 private string EscapeJs(
string s) => s?.Replace(
"\\",
"\\\\").Replace(
"'",
"\\'").Replace(
"\"",
"\\\"") ??
"";
37 internal SeleniumStorageHelper(
Browser browser)
40 if (Enums.BrowserType.FireFox != browser.BrowserType)
41 browserDriver = (ChromiumDriver) browser.BrowserDriver;
42 this.browser = browser;
53 return action.StorageType
switch
55 WebsiteStorageType.indexedDb => GetIndexedDb(action),
56 WebsiteStorageType.cookie => GetCookies(action),
57 WebsiteStorageType.localStorage => GetLocalOrSessionStorage(action),
58 WebsiteStorageType.sessionStorage => GetLocalOrSessionStorage(action),
59 WebsiteStorageType.cache => GetCache(action),
60 WebsiteStorageType.notSet => ReturnEmptyString(),
61 _ => FailAndReturnNull($
"Get not implemented for {action.StorageType}", action)
66 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to get [{action.StorageType}]", action, GPALObjectType.Other, ex);
76 deleted = action.StorageType
switch
78 WebsiteStorageType.indexedDb => DeleteIndexedDb(action),
79 WebsiteStorageType.cookie => DeleteCookies(action),
80 WebsiteStorageType.localStorage => DeleteLocalOrSessionStorage(action),
81 WebsiteStorageType.sessionStorage => DeleteLocalOrSessionStorage(action),
82 WebsiteStorageType.cache => DeleteCache(action),
83 WebsiteStorageType.notSet => ReturnZero(),
84 _ => FailAndReturnZero($
"Delete not implemented for {action.StorageType}", action)
89 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to delete [{action.StorageType}]", action, GPALObjectType.Other, ex);
98 return action.StorageType
switch
100 WebsiteStorageType.indexedDb => SetIndexedDb(action),
101 WebsiteStorageType.cookie => SetCookie(action),
102 WebsiteStorageType.localStorage => SetLocalOrSessionStorage(action),
103 WebsiteStorageType.sessionStorage => SetLocalOrSessionStorage(action),
104 WebsiteStorageType.cache => SetCache(action),
105 WebsiteStorageType.notSet => ReturnFalse(),
106 _ => FailAndReturnFalse($
"Set not implemented for {action.StorageType}", action)
111 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to set [{action.StorageType}]", action, GPALObjectType.Other, ex);
116 private bool ReturnFalse()
121 private int ReturnZero()
126 private string ReturnEmptyString()
134 private string GetIndexedDb(
StorageAction action) => GetIndexedDbSelenium(action);
138 string dbName =
string.IsNullOrWhiteSpace(action.
Path) ? null : EscapeJs(action.
Path);
139 string storeName =
string.IsNullOrWhiteSpace(action.
StoreName) ? null : EscapeJs(action.
StoreName);
140 string key =
string.IsNullOrWhiteSpace(action.
Key) ? null : EscapeJs(action.
Key);
143 string JsLit(
string value) => value ==
null ?
"null" : $
"'{value}'";
145 string expression =
$@"
148 const openDb = (name) => new Promise((resolve, reject) => {{
150 reject(new Error('Database name is required'));
153 const req = indexedDB.open(name);
154 req.onsuccess = () => resolve(req.result);
155 req.onerror = () => reject(req.error);
159 // CASE 1: No dbName > list all database names
160 if ({JsLit(dbName)} === null) {{
161 if (typeof indexedDB.databases === 'function') {{
162 const dbs = await indexedDB.databases();
163 return JSON.stringify(dbs.map(db => db.name));
165 return JSON.stringify([]); // fallback
168 // CASE 2: dbName given, but no storeName > list object stores
169 if ({JsLit(storeName)} === null) {{
170 const db = await openDb({JsLit(dbName)});
171 const storeNames = Array.from(db.objectStoreNames);
173 return JSON.stringify(storeNames);
176 // CASE 3: Both dbName + storeName given
177 const db = await openDb({JsLit(dbName)});
179 if (!db.objectStoreNames.contains({JsLit(storeName)})) {{
181 return {(key != null ? "null" : "JSON.stringify([])
")};
184 const tx = db.transaction({JsLit(storeName)}, 'readonly');
185 const store = tx.objectStore({JsLit(storeName)});
187 if ({(key != null ? "true" : "false")}) {{
188 // Get single value by key
189 const value = await new Promise((resolve, reject) => {{
190 const req = store.get({JsLit(key)});
191 req.onsuccess = () => resolve(req.result ?? null);
192 req.onerror = () => reject(req.error);
195 return JSON.stringify(value);
198 // Get ALL values from the store
199 const values = await new Promise((resolve, reject) => {{
200 const req = store.getAll();
201 req.onsuccess = () => resolve(req.result);
202 req.onerror = () => reject(req.error);
205 return JSON.stringify(values);
208 console.error('IndexedDB error in CDP:', e);
209 return JSON.stringify({{
211 message: e.message || 'Unknown error',
212 detail: e.name || e.toString()
218 if (Enums.BrowserType.FireFox == browser.BrowserType)
220 string retVal = ExecuteFirefoxAsyncScript(expression);
221 if (retVal?.Contains(
"\"error\":true") ==
true)
222 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB operation failed [{retVal}]", browser, GPALObjectType.Browser);
223 action.Data = retVal;
227 var parameters =
new Dictionary<string, object>
229 {
"expression", expression },
230 {
"awaitPromise",
true },
231 {
"returnByValue",
true }
236 dynamic cdpResult = ((ChromiumDriver)browserDriver).ExecuteCdpCommand(
"Runtime.evaluate", parameters);
238 string retVal = ExtractResultValue(cdpResult);
241 if (retVal?.Contains(
"\"error\":true") ==
true)
242 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB operation failed [{retVal}]", cdpResult, GPALObjectType.Other);
244 action.Data = retVal;
249 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"IndexedDB operation failed", action, GPALObjectType.Other, ex);
250 string errorJson =
$@"{{""error"":true,""message"":""{EscapeJs(ex.Message)}"",""detail"":""CDP execution error""}}";
251 action.Data = errorJson;
258 bool hasDb = !
string.IsNullOrWhiteSpace(action.
Path);
259 bool hasStore = !
string.IsNullOrWhiteSpace(action.
StoreName);
260 bool hasKey = !
string.IsNullOrWhiteSpace(action.
Key);
268 if (Enums.BrowserType.FireFox == browser.BrowserType)
270 if (
true ==
string.IsNullOrEmpty(js))
271 return DeleteAllIndexedDbFirefox(action) ? 1 : 0;
272 string ffRaw = ExecuteFirefoxAsyncScript(js) ??
"0";
273 if (IsErrorJson(ffRaw))
275 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB delete failed: [{ffRaw}]", action, GPALObjectType.Other);
278 return int.TryParse(ffRaw, out
int ffN) ? ffN : 0;
283 if (
true ==
string.IsNullOrEmpty(js))
284 return DeleteAllIndexedDb(action) ? 1 : 0;
286 string raw = ExecuteAsyncJs<string>(js) ??
"0";
288 if (IsErrorJson(raw))
290 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB delete failed: [{raw}]", action, GPALObjectType.Other);
294 return int.TryParse(raw, out
int n) ? n : 0;
301 if (
true ==
string.IsNullOrEmpty(origin))
304 if (
string.IsNullOrEmpty(origin))
306 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Unable to determine current origin for [{action.StorageType}] d[{action.Domain}] sn[{action.StoreName}] p[{action.Path}] k[{action.Key}]");
310 var cdpParams =
new Dictionary<string, object>()
312 {
"origin", origin },
313 {
"storageTypes",
"indexeddb" }
316 dynamic cdpResult = ((ChromiumDriver)browserDriver).ExecuteCdpCommand(
"Storage.clearDataForOrigin", cdpParams);
318 string retVal = ExtractResultValue(cdpResult);
320 if (retVal?.Contains(
"\"error\":true") ==
true)
322 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB operation failed [{retVal}]", cdpResult, GPALObjectType.Other);
331 string js =
@"(async () => {
333 if (typeof indexedDB.databases !== 'function') return 'ok';
334 const dbs = await indexedDB.databases();
335 for (const db of dbs) {
336 await new Promise((res, rej) => {
337 const req = indexedDB.deleteDatabase(db.name);
345 return JSON.stringify({error: true, message: e.message});
348 string result = ExecuteFirefoxAsyncScript(js);
349 if (result?.Contains(
"\"error\":true") ==
true)
351 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB delete all failed [{result}]", action, GPALObjectType.Other);
359 if (
string.IsNullOrWhiteSpace(action.
StoreName) ||
string.IsNullOrWhiteSpace(action.
Key))
360 return FailAndReturnFalse(
"SetCache requires StoreName (cache) and Key (request URL)", action);
363 string cacheName = EscapeJs(action.
StoreName);
364 string requestUrl = EscapeJs(action.
Key);
365 string data = EscapeJs(action.
Data);
370 function fail(msg, err) {{
371 console.error(msg, err?.message || err || '');
372 return {{ error: true, message: msg }};
375 function parseBody(v) {{
377 return JSON.parse(v);
383 function buildResponse(body) {{
384 // Normalize body > string
386 let contentType = 'text/plain;charset=utf-8';
388 if (typeof body === 'object' && body !== null) {{
389 payload = JSON.stringify(body);
390 contentType = 'application/json;charset=utf-8';
392 payload = String(body);
395 return new Response(payload, {{
399 'Content-Type': contentType,
400 'Cache-Control': 'max-age=31536000'
405 function normalizeUrl(url) {{
407 return new URL(url, location.origin).href;
409 throw new Error('Invalid URL: ' + url);
414 const cache = await caches.open('{cacheName}');
418 normalizedUrl = normalizeUrl('{requestUrl}');
420 return fail('URL normalization failed', e);
423 const request = new Request(normalizedUrl, {{
427 const body = parseBody('{data}');
428 const response = buildResponse(body);
431 await cache.put(request, response);
433 return fail('cache.put failed', e);
438 cacheName: '{cacheName}',
443 return fail('Cache set failed', err);
449 if (Enums.BrowserType.FireFox == browser.BrowserType)
451 string ffResult = ExecuteFirefoxAsyncScript(js);
452 if (IsErrorJson(ffResult ??
""))
454 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Cache set failed: [{ffResult}]", action, GPALObjectType.Other);
457 return ffResult?.Contains(
"success") ==
true;
460 string result = ExecuteAsyncJs<string>(js) ??
"false";
462 if (IsErrorJson(result))
464 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Cache set failed: [{result}]", action, GPALObjectType.Other);
468 return result.Contains(
"success");
473 string db = EscapeJs(action.
Path);
474 string store = EscapeJs(action.
StoreName);
475 string key = EscapeJs(action.
Key);
476 string val = EscapeJs(action.
Data);
480 function fail(msg, err, db) {{
481 console.error(msg, err?.name || err || '');
482 try {{ db?.close(); }} catch {{ }}
483 return {{ error: true, message: msg }};
486 function parseValue(v) {{
487 try {{ return JSON.parse(v); }} catch {{ return v; }}
490 function isValidKey(k) {{
491 if (k === undefined) return false;
492 if (typeof k === 'string' || typeof k === 'number') return true;
493 if (k instanceof Date) return true;
494 if (Array.isArray(k)) return true;
500 let db = await new Promise((res, rej) => {{
501 let r = indexedDB.open('{db}');
502 r.onsuccess = e => res(e.target.result);
503 r.onupgradeneeded = e => {{
504 if (!e.target.result.objectStoreNames.contains('{store}'))
505 e.target.result.createObjectStore('{store}', {{ keyPath: null }});
507 r.onerror = e => rej(e.target.error);
510 let tx = db.transaction('{store}', 'readwrite');
511 let objStore = tx.objectStore('{store}');
512 let keyPath = objStore.keyPath;
514 let value = parseValue('{val}');
515 let inputKey = '{key}';
516 if (inputKey === '') inputKey = undefined;
517 if (inputKey !== undefined && !isValidKey(inputKey))
518 return fail('Invalid key type', inputKey, db);
520 // Wrap value for inline keyPath if needed
521 if (keyPath !== null && (typeof value !== 'object' || value === null)) {{
522 const wrapped = {{}};
523 if (typeof keyPath === 'string') wrapped[keyPath] = inputKey;
524 else if (Array.isArray(keyPath)) {{
525 if (!Array.isArray(inputKey)) throw new Error('Compound key requires array key');
526 for (let i = 0; i < keyPath.length; i++) wrapped[keyPath[i]] = inputKey[i];
528 wrapped.value = value;
530 inputKey = undefined;
535 if (keyPath === null) {{
536 if (inputKey === undefined && !objStore.autoIncrement)
537 return fail('Missing key for out-of-line store', null, db);
538 req = objStore.put(value, inputKey);
540 req = objStore.put(value);
543 return fail('Put setup error', e, db);
546 // Wait for transaction completion
547 await new Promise((res, rej) => {{
548 tx.oncomplete = () => res(true);
549 tx.onerror = () => rej(tx.error);
550 tx.onabort = () => rej(tx.error || 'Transaction aborted');
557 return {{ error: true, message: e.message }};
562 if (Enums.BrowserType.FireFox == browser.BrowserType)
564 string ffResult = ExecuteFirefoxAsyncScript(js);
565 if (ffResult ==
null || IsErrorJson(ffResult) || ffResult ==
"false")
567 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB set failed: [{ffResult}]", action, GPALObjectType.Other);
573 var parms =
new Dictionary<string, object>
575 {
"expression", js },
576 {
"awaitPromise",
true },
577 {
"returnByValue",
true }
580 dynamic result = ((ChromiumDriver)browserDriver).ExecuteCdpCommand(
"Runtime.evaluate", parms);
582 if (
false == result?[
"result"]?[
"value"])
584 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB set failed: [{result}]", action, GPALObjectType.Other);
594 private string BuildDeleteKeysJs(
string dbPat,
string storePat,
string keyPat)
596 string keyJs = $
"'{EscapeJs(keyPat)}'";
600 string collect =
"deleted++";
602 string dbLoop =
string.IsNullOrWhiteSpace(dbPat)
603 ?
@"let dbs = await indexedDB.databases();
604 for (let info of dbs) { let dbName = info.name;"
605 : $
"let dbName = '{EscapeJs(dbPat)}';";
607 string dbClose =
string.IsNullOrWhiteSpace(dbPat) ?
"db.close(); }" :
"db.close();";
609 string storeLoop =
string.IsNullOrWhiteSpace(storePat)
610 ?
@"for (let storeName of db.objectStoreNames) {"
611 : $
"let storeName = '{EscapeJs(storePat)}';";
613 string storeClose =
string.IsNullOrWhiteSpace(storePat) ?
"}" :
"";
620 let db = await new Promise((res, rej) => {{
621 let r = indexedDB.open(dbName);
622 r.onsuccess = e => res(e.target.result);
623 r.onerror = e => rej(e.target.error);
627 let tx = db.transaction(storeName, 'readwrite');
628 let store = tx.objectStore(storeName);
629 let exists = await new Promise((res, rej) => {{
630 let r = store.get({keyJs});
631 r.onsuccess = () => res(r.result !== undefined);
635 await new Promise((res, rej) => {{
636 let r = store.delete({keyJs});
642 }} catch (e) {{ /* skip bad store */ }}
647 return JSON.stringify({{error:true, message:e.message}});
653 private string BuildClearStoreJs(
string dbNameRaw,
string storeNameRaw)
655 string db = EscapeJs(dbNameRaw);
656 string store = EscapeJs(storeNameRaw);
661 let db = await new Promise((res, rej) => {{
662 let r = indexedDB.open('{db}');
663 r.onsuccess = e => res(e.target.result);
664 r.onerror = e => rej(e.target.error);
666 let count = await new Promise((res, rej) => {{
667 let req = db.transaction('{store}', 'readonly').objectStore('{store}').count();
668 req.onsuccess = () => res(req.result);
671 await new Promise((res, rej) => {{
672 let tx = db.transaction('{store}', 'readwrite');
673 let req = tx.objectStore('{store}').clear();
680 return JSON.stringify({{error:true, message:e.message}});
695 bool isAllKey =
string.IsNullOrEmpty(action.
Key);
696 var cookies = webDriver.Manage().Cookies.AllCookies
698 (isAllKey || c.Name == action.
Key) &&
699 UrlHelper.CookieDomainMatches(c.Domain, action.
Domain) &&
700 (
string.IsNullOrEmpty(action.
Path) || c.Path == action.
Path))
703 if (!cookies.Any())
return "[]";
707 string data = System.Text.Json.JsonSerializer.Serialize(cookies.Select(c =>
new
715 httpOnly = c.IsHttpOnly
728 bool isAllKey =
string.IsNullOrEmpty(action.
Key);
730 if (Enums.BrowserType.FireFox == browser.BrowserType)
733 if (isAllKey &&
string.IsNullOrEmpty(action.
Domain) &&
string.IsNullOrEmpty(action.
Path))
735 int count = webDriver.Manage().Cookies.AllCookies.Count;
736 webDriver.Manage().Cookies.DeleteAllCookies();
739 foreach (var c
in webDriver.Manage().Cookies.AllCookies.ToList())
741 if (!isAllKey && c.Name != action.
Key)
continue;
742 if (!UrlHelper.CookieDomainMatches(c.Domain, action.
Domain))
continue;
743 if (!
string.IsNullOrEmpty(action.
Path) && c.Path != action.
Path)
continue;
744 webDriver.Manage().Cookies.DeleteCookieNamed(c.Name);
747 if (!isAllKey && deleted == 0)
748 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Cookie deletion failed: key [{action.Key}] not found", action, GPALObjectType.Other);
758 dynamic response = ((ChromiumDriver)browserDriver).ExecuteCdpCommand(
"Network.getCookies",
new Dictionary<string, object>());
759 var cookies = response[
"cookies"] as Array;
761 if (cookies ==
null)
return 0;
763 foreach (dynamic c
in cookies)
765 string name = (string)c[
"name"];
766 string domain = (string)c[
"domain"];
767 string path = (string)c[
"path"];
770 if (!isAllKey && name != action.
Key)
continue;
771 if (!UrlHelper.CookieDomainMatches(domain, action.
Domain))
continue;
772 if (!
string.IsNullOrEmpty(action.
Path) && path != action.
Path)
continue;
775 var delParams =
new Dictionary<string, object>
778 {
"domain", domain },
783 if (c.ContainsKey(
"partitionKey") && c[
"partitionKey"] !=
null)
785 var pk =
new Dictionary<string, object>
787 {
"topLevelSite", (string)c[
"partitionKey"][
"topLevelSite"] },
788 {
"hasCrossSiteAncestor", (bool)c[
"partitionKey"][
"hasCrossSiteAncestor"] }
790 delParams.Add(
"partitionKey", pk);
794 ((ChromiumDriver)browserDriver).ExecuteCdpCommand(
"Network.deleteCookies", delParams);
798 if (!isAllKey && deletedCdp == 0)
800 $
"Cookie deletion failed: key [{action.Key}] not found",
801 action, GPALObjectType.Other);
809 if (Enums.BrowserType.FireFox == browser.BrowserType)
811 string name = action.
Key;
812 string value = action.
Data;
813 if (
string.IsNullOrWhiteSpace(name) ||
string.IsNullOrWhiteSpace(value))
815 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[key] and [data] cannot be empty. Unable to set [{action.StorageType}]", action, GPALObjectType.Other);
818 string path =
string.IsNullOrWhiteSpace(action.
Path) ?
"/" : action.
Path;
819 string domain =
string.IsNullOrEmpty(action.
Domain) ?
new Uri(webDriver.Url).Host : action.
Domain;
820 webDriver.Manage().Cookies.AddCookie(
new Cookie(name, value, domain, path,
null));
826 string name = action.
Key;
827 string value = action.
Data;
828 string path =
string.IsNullOrWhiteSpace(action.
Path) ?
"/" : action.
Path;
829 string domainInput = action.
Domain;
832 var emptyFields =
new List<string>();
834 if (
string.IsNullOrWhiteSpace(name))
835 emptyFields.Add(
"key");
837 if (
string.IsNullOrWhiteSpace(value))
838 emptyFields.Add(
"data");
840 if (
string.IsNullOrWhiteSpace(path))
841 emptyFields.Add(
"path");
843 if (
string.IsNullOrWhiteSpace(domainInput))
844 emptyFields.Add(
"domain");
847 string emptyCsv =
string.Join(
",", emptyFields);
850 if (0 < emptyFields.Count)
852 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[{emptyCsv}] cannot be empty. Unable to set [{action.StorageType}]", action, GPALObjectType.Other);
856 var driver = (OpenQA.Selenium.Chromium.ChromiumDriver)browserDriver;
859 var getParams =
new Dictionary<string, object>
861 [
"urls"] =
new[] { driver.Url }
864 dynamic result = driver.ExecuteCdpCommand(
"Storage.getCookies", getParams);
865 var cookies = result[
"cookies"] as
object[];
867 dynamic existing =
null;
871 foreach (dynamic c
in cookies)
873 string cName = c[
"name"]?.ToString() ??
"";
874 string cDomain = c[
"domain"]?.ToString() ??
"";
875 string cPath = c[
"path"]?.ToString() ??
"/";
877 bool domainMatches =
string.IsNullOrEmpty(domainInput)
878 || cDomain == domainInput