29 public class SeleniumStorageHelper
31 internal IWebDriver webDriver;
32 internal ChromiumDriver browserDriver;
34 private string EscapeJs(
string s) => s?.Replace(
"\\",
"\\\\").Replace(
"'",
"\\'").Replace(
"\"",
"\\\"") ??
"";
36 internal SeleniumStorageHelper(
Browser browser)
39 if (Enums.BrowserType.FireFox != browser.BrowserType)
40 browserDriver = (ChromiumDriver) browser.BrowserDriver;
41 this.browser = browser;
52 return action.StorageType
switch
54 WebsiteStorageType.indexedDb => GetIndexedDb(action),
55 WebsiteStorageType.cookie => GetCookies(action),
56 WebsiteStorageType.localStorage => GetLocalOrSessionStorage(action),
57 WebsiteStorageType.sessionStorage => GetLocalOrSessionStorage(action),
58 WebsiteStorageType.cache => GetCache(action),
59 WebsiteStorageType.notSet => ReturnEmptyString(),
60 _ => FailAndReturnNull($
"Get not implemented for {action.StorageType}", action)
65 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to get [{action.StorageType}]", action, GPALObjectType.Other, ex);
75 deleted = action.StorageType
switch
77 WebsiteStorageType.indexedDb => DeleteIndexedDb(action),
78 WebsiteStorageType.cookie => DeleteCookies(action),
79 WebsiteStorageType.localStorage => DeleteLocalOrSessionStorage(action),
80 WebsiteStorageType.sessionStorage => DeleteLocalOrSessionStorage(action),
81 WebsiteStorageType.cache => DeleteCache(action),
82 WebsiteStorageType.notSet => ReturnZero(),
83 _ => FailAndReturnZero($
"Delete not implemented for {action.StorageType}", action)
88 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to delete [{action.StorageType}]", action, GPALObjectType.Other, ex);
97 return action.StorageType
switch
99 WebsiteStorageType.indexedDb => SetIndexedDb(action),
100 WebsiteStorageType.cookie => SetCookie(action),
101 WebsiteStorageType.localStorage => SetLocalOrSessionStorage(action),
102 WebsiteStorageType.sessionStorage => SetLocalOrSessionStorage(action),
103 WebsiteStorageType.cache => SetCache(action),
104 WebsiteStorageType.notSet => ReturnFalse(),
105 _ => FailAndReturnFalse($
"Set not implemented for {action.StorageType}", action)
110 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to set [{action.StorageType}]", action, GPALObjectType.Other, ex);
115 private bool ReturnFalse()
120 private int ReturnZero()
125 private string ReturnEmptyString()
133 private string GetIndexedDb(
StorageAction action) => GetIndexedDbSelenium(action);
137 string dbName =
string.IsNullOrWhiteSpace(action.
Path) ? null : EscapeJs(action.
Path);
138 string storeName =
string.IsNullOrWhiteSpace(action.
StoreName) ? null : EscapeJs(action.
StoreName);
139 string key =
string.IsNullOrWhiteSpace(action.
Key) ? null : EscapeJs(action.
Key);
142 string JsLit(
string value) => value ==
null ?
"null" : $
"'{value}'";
144 string expression =
$@"
147 const openDb = (name) => new Promise((resolve, reject) => {{
149 reject(new Error('Database name is required'));
152 const req = indexedDB.open(name);
153 req.onsuccess = () => resolve(req.result);
154 req.onerror = () => reject(req.error);
158 // CASE 1: No dbName > list all database names
159 if ({JsLit(dbName)} === null) {{
160 if (typeof indexedDB.databases === 'function') {{
161 const dbs = await indexedDB.databases();
162 return JSON.stringify(dbs.map(db => db.name));
164 return JSON.stringify([]); // fallback
167 // CASE 2: dbName given, but no storeName > list object stores
168 if ({JsLit(storeName)} === null) {{
169 const db = await openDb({JsLit(dbName)});
170 const storeNames = Array.from(db.objectStoreNames);
172 return JSON.stringify(storeNames);
175 // CASE 3: Both dbName + storeName given
176 const db = await openDb({JsLit(dbName)});
178 if (!db.objectStoreNames.contains({JsLit(storeName)})) {{
180 return {(key != null ? "null" : "JSON.stringify([])
")};
183 const tx = db.transaction({JsLit(storeName)}, 'readonly');
184 const store = tx.objectStore({JsLit(storeName)});
186 if ({(key != null ? "true" : "false")}) {{
187 // Get single value by key
188 const value = await new Promise((resolve, reject) => {{
189 const req = store.get({JsLit(key)});
190 req.onsuccess = () => resolve(req.result ?? null);
191 req.onerror = () => reject(req.error);
194 return JSON.stringify(value);
197 // Get ALL values from the store
198 const values = await new Promise((resolve, reject) => {{
199 const req = store.getAll();
200 req.onsuccess = () => resolve(req.result);
201 req.onerror = () => reject(req.error);
204 return JSON.stringify(values);
207 console.error('IndexedDB error in CDP:', e);
208 return JSON.stringify({{
210 message: e.message || 'Unknown error',
211 detail: e.name || e.toString()
217 if (Enums.BrowserType.FireFox == browser.BrowserType)
219 string retVal = ExecuteFirefoxAsyncScript(expression);
220 if (retVal?.Contains(
"\"error\":true") ==
true)
221 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB operation failed [{retVal}]", browser, GPALObjectType.Browser);
222 action.Data = retVal;
226 var parameters =
new Dictionary<string, object>
228 {
"expression", expression },
229 {
"awaitPromise",
true },
230 {
"returnByValue",
true }
235 dynamic cdpResult = ((ChromiumDriver)browserDriver).ExecuteCdpCommand(
"Runtime.evaluate", parameters);
237 string retVal = ExtractResultValue(cdpResult);
240 if (retVal?.Contains(
"\"error\":true") ==
true)
241 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB operation failed [{retVal}]", cdpResult, GPALObjectType.Other);
243 action.Data = retVal;
248 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"IndexedDB operation failed", action, GPALObjectType.Other, ex);
249 string errorJson =
$@"{{""error"":true,""message"":""{EscapeJs(ex.Message)}"",""detail"":""CDP execution error""}}";
250 action.Data = errorJson;
257 bool hasDb = !
string.IsNullOrWhiteSpace(action.
Path);
258 bool hasStore = !
string.IsNullOrWhiteSpace(action.
StoreName);
259 bool hasKey = !
string.IsNullOrWhiteSpace(action.
Key);
267 if (Enums.BrowserType.FireFox == browser.BrowserType)
269 if (
true ==
string.IsNullOrEmpty(js))
270 return DeleteAllIndexedDbFirefox(action) ? 1 : 0;
271 string ffRaw = ExecuteFirefoxAsyncScript(js) ??
"0";
272 if (IsErrorJson(ffRaw))
274 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB delete failed: [{ffRaw}]", action, GPALObjectType.Other);
277 return int.TryParse(ffRaw, out
int ffN) ? ffN : 0;
280 if (
true ==
string.IsNullOrEmpty(js))
281 DeleteAllIndexedDb(action);
284 string raw = ExecuteAsyncJs<string>(js) ??
"0";
286 if (IsErrorJson(raw))
288 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"IndexedDB delete failed: [{raw}]", action, GPALObjectType.Other);
292 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);
588 return result ==
"true";
591 private string BuildDeleteKeysJs(
string dbPat,
string storePat,
string keyPat,
bool provenance)
593 string keyJs = $
"'{EscapeJs(keyPat)}'";
594 string collect = provenance
595 ? $
"results.push({{db: dbName, store: storeName, key: {keyJs}, deleted: true}})"
598 string dbLoop =
string.IsNullOrWhiteSpace(dbPat)
599 ?
@"let dbs = await indexedDB.databases();
600 for (let info of dbs) { let dbName = info.name;"
601 : $
"let dbName = '{EscapeJs(dbPat)}';";
603 string dbClose =
string.IsNullOrWhiteSpace(dbPat) ?
"db.close(); }" :
"db.close();";
605 string storeLoop =
string.IsNullOrWhiteSpace(storePat)
606 ?
@"for (let storeName of db.objectStoreNames) {"
607 : $
"let storeName = '{EscapeJs(storePat)}';";
609 string storeClose =
string.IsNullOrWhiteSpace(storePat) ?
"}" :
"";
617 let db = await new Promise((res, rej) => {{
618 let r = indexedDB.open(dbName);
619 r.onsuccess = e => res(e.target.result);
620 r.onerror = e => rej(e.target.error);
624 let tx = db.transaction(storeName, 'readwrite');
625 let store = tx.objectStore(storeName);
626 let exists = await new Promise((res, rej) => {{
627 let r = store.get({keyJs});
628 r.onsuccess = () => res(r.result !== undefined);
632 await new Promise((res, rej) => {{
633 let r = store.delete({keyJs});
639 }} catch (e) {{ /* skip bad store */ }}
642 return {(provenance ? "JSON.stringify(results)
" : "deleted
")};
644 return JSON.stringify({{error:true, message:e.message}});
650 private string BuildClearStoreJs(
string dbNameRaw,
string storeNameRaw)
652 string db = EscapeJs(dbNameRaw);
653 string store = EscapeJs(storeNameRaw);
658 let db = await new Promise((res, rej) => {{
659 let r = indexedDB.open('{db}');
660 r.onsuccess = e => res(e.target.result);
661 r.onerror = e => rej(e.target.error);
663 let count = await new Promise((res, rej) => {{
664 let req = db.transaction('{store}', 'readonly').objectStore('{store}').count();
665 req.onsuccess = () => res(req.result);
668 await new Promise((res, rej) => {{
669 let tx = db.transaction('{store}', 'readwrite');
670 let req = tx.objectStore('{store}').clear();
677 return JSON.stringify({{error:true, message:e.message}});
692 bool isAllKey =
string.IsNullOrEmpty(action.
Key);
693 var cookies = webDriver.Manage().Cookies.AllCookies
695 (isAllKey || c.Name == action.
Key) &&
696 UrlHelper.CookieDomainMatches(c.Domain, action.
Domain) &&
697 (
string.IsNullOrEmpty(action.
Path) || c.Path == action.
Path))
700 if (!cookies.Any())
return "[]";
703 ? System.Text.Json.JsonSerializer.Serialize(cookies.Select(c =>
new
711 httpOnly = c.IsHttpOnly
713 : System.Text.Json.JsonSerializer.Serialize(cookies.Select(c => c.Name));
721 bool isAllKey =
string.IsNullOrEmpty(action.
Key);
723 if (Enums.BrowserType.FireFox == browser.BrowserType)
726 if (isAllKey &&
string.IsNullOrEmpty(action.
Domain) &&
string.IsNullOrEmpty(action.
Path))
728 int count = webDriver.Manage().Cookies.AllCookies.Count;
729 webDriver.Manage().Cookies.DeleteAllCookies();
732 foreach (var c
in webDriver.Manage().Cookies.AllCookies.ToList())
734 if (!isAllKey && c.Name != action.
Key)
continue;
735 if (!UrlHelper.CookieDomainMatches(c.Domain, action.
Domain))
continue;
736 if (!
string.IsNullOrEmpty(action.
Path) && c.Path != action.
Path)
continue;
737 webDriver.Manage().Cookies.DeleteCookieNamed(c.Name);
740 if (!isAllKey && deleted == 0)
741 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Cookie deletion failed: key [{action.Key}] not found", action, GPALObjectType.Other);
751 dynamic response = ((ChromiumDriver)browserDriver).ExecuteCdpCommand(
"Network.getCookies",
new Dictionary<string, object>());
752 var cookies = response[
"cookies"] as Array;
754 if (cookies ==
null)
return 0;
756 foreach (dynamic c
in cookies)
758 string name = (string)c[
"name"];
759 string domain = (string)c[
"domain"];
760 string path = (string)c[
"path"];
763 if (!isAllKey && name != action.
Key)
continue;
764 if (!UrlHelper.CookieDomainMatches(domain, action.
Domain))
continue;
765 if (!
string.IsNullOrEmpty(action.
Path) && path != action.
Path)
continue;
768 var delParams =
new Dictionary<string, object>
771 {
"domain", domain },
776 if (c.ContainsKey(
"partitionKey") && c[
"partitionKey"] !=
null)
778 var pk =
new Dictionary<string, object>
780 {
"topLevelSite", (string)c[
"partitionKey"][
"topLevelSite"] },
781 {
"hasCrossSiteAncestor", (bool)c[
"partitionKey"][
"hasCrossSiteAncestor"] }
783 delParams.Add(
"partitionKey", pk);
787 ((ChromiumDriver)browserDriver).ExecuteCdpCommand(
"Network.deleteCookies", delParams);
791 if (!isAllKey && deletedCdp == 0)
793 $
"Cookie deletion failed: key [{action.Key}] not found",
794 action, GPALObjectType.Other);
802 if (Enums.BrowserType.FireFox == browser.BrowserType)
804 string name = action.
Key;
805 string value = action.
Data;
806 if (
string.IsNullOrWhiteSpace(name) ||
string.IsNullOrWhiteSpace(value))
808 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[key] and [data] cannot be empty. Unable to set [{action.StorageType}]", action, GPALObjectType.Other);
811 string path =
string.IsNullOrWhiteSpace(action.
Path) ?
"/" : action.
Path;
812 string domain =
string.IsNullOrEmpty(action.
Domain) ?
new Uri(webDriver.Url).Host : action.
Domain;
813 webDriver.Manage().Cookies.AddCookie(
new Cookie(name, value, domain, path,
null));
819 string name = action.
Key;
820 string value = action.
Data;
821 string path =
string.IsNullOrWhiteSpace(action.
Path) ?
"/" : action.
Path;
822 string domainInput = action.
Domain;
825 var emptyFields =
new List<string>();
827 if (
string.IsNullOrWhiteSpace(name))
828 emptyFields.Add(
"key");