106 public delegate CallIfStatus
CallIfDelegate(
IBrowser browser, List<IGPALElement> foundElements, List<IGPALElement> matchedElements,
Selector selector,
bool matchedAll);
190 #region <Working Vars>
195 internal UnitOfWork persistentUOW {
get;
set; } =
null;
201 internal DateTime? BrowserLaunched {
get;
set; } =
null;
206 internal int tabsWeOpened {
get;
set; } = 0;
213 internal string GhostTabUrl {
get;
set; } =
null;
218 internal string GhostTabHandle {
get;
set; } =
null;
228 private readonly Dictionary<string, string> tabWindows =
new Dictionary<string, string>();
230 internal bool _areRobotsAllowed =
true;
235 public bool AreRobotsAllowed {
get => _areRobotsAllowed;
internal set => _areRobotsAllowed = value; }
237 #endregion <Working Vars>
263 DriverLocation =
GPAL.DriverLocation,
264 DownloadTimeoutInSec = GPAL.GPALSettings.DownloadTimeoutInSec
268 persistentUOW =
new UnitOfWork() { Browser =
this };
269 WorkflowManager =
new WorkflowManager(
this);
272 #region <Browser Settings>
299 BrowserSettings.UseDirectDownload = trueFalse;
309 BrowserSettings.DownloadTimeoutInSec = seconds;
338 BrowserSettings.BlockPopUps = blockPopUp;
364 BrowserSettings.HiddenDesktop = hiddenDesktop;
368 if (
true == hiddenDesktop)
369 BrowserSettings.HiddenDesktopName = Desktops.NextName();
390 BrowserSettings.HiddenDesktop =
true;
394 BrowserSettings.HiddenDesktopName =
true ==
string.IsNullOrWhiteSpace(desktopName)
395 ? Desktops.NextName()
407 private T LaunchWhereAsked<T>(Func<T> launch)
487 [
BrowserType(Description =
"Chrome, Edge or Firefox")]
490 BrowserSettings.BrowserType = browserType;
510 BrowserSettings.DeleteFileBeforeDownload = overwriteFile;
529 BrowserSettings.LoadImages = loadImages;
551 BrowserSettings.ScrollIntoView = trueFalse;
572 BrowserSettings.PromptForDownload = promptForDownload;
582 BrowserSettings.OpenPDFExternally = openPDFExternally;
625 if (
true == CurrentUOW.ActionCalled)
627 var iframeRoot = CurrentUOW.ContextPath;
628 var shadowRoot = CurrentUOW.ShadowRoot;
632 ContextPath = iframeRoot,
633 ShadowRoot = shadowRoot,
638 if (
null != inputGrid && 0 < inputGrid.Count())
641 CurrentUOW.RetGrid = inputGrid;
642 CurrentUOW.ColCount = inputGrid.Columns;
645 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Input IGPALGrid<T> is empty.",
this, GPALObjectType.Browser);
685 BrowserSettings.UseExistingBrowser =
true;
686 BrowserSettings.ExistingBrowserPort = port;
690 BrowserSettings.DebugPort = port;
740 if (
true ==
string.IsNullOrWhiteSpace(restApiUrl))
742 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No address given to WithGPALRestAPIUrl, so a browser will be launched as usual",
this, GPALObjectType.Browser);
748 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"WithGPALRestAPIUrl drives a browser over GPALRestAPI, which is the OttoMagic engine. The engine is currently [{BrowserSettings.AutomationEngine}]",
this, GPALObjectType.Browser);
750 BrowserSettings.RestApiBaseUrl = restApiUrl.EndsWith(
"/") ? restApiUrl : restApiUrl +
"/";
751 BrowserSettings.AttachedRestApi =
true;
753 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Driving the browser already running on [{BrowserSettings.RestApiBaseUrl}].",
this, GPALObjectType.Browser);
760 if (port < 1024 || port > 65535)
762 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Invalid debug port [{port}]. Must be between 1024 and 65535. Using current port [{BrowserSettings.DebugPort}].",
this, GPALObjectType.Browser);
767 while (
true == BrowserHelper.IsPortListening(port))
770 BrowserSettings.DebugPort = port;
772 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Puppeteer using port [{port}]",
this, GPALObjectType.Browser);
779 BrowserSettings.ExistingBrowserPort = port;
782 BrowserSettings.DebugPipe =
false;
800 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Firefox cannot be driven with [{automationEngine}], so this browser runs on [{steppedDownTo}]",
this, GPALObjectType.Browser);
802 automationEngine = steppedDownTo;
818 BrowserSettings.DebugPipe =
true;
819 BrowserSettings.DebugPort =
null;
842 BrowserSettings.WaitOnDocumentReady =
true;
843 BrowserSettings.WaitOnNetworkIdle =
false;
844 BrowserSettings.WaitForDocumentReadyTimeoutMs = timeoutInMs;
866 BrowserSettings.NavigationGraceMs = graceInMs;
892 BrowserSettings.WaitOnNetworkIdle = trueFalse;
893 BrowserSettings.WaitOnDocumentReady = trueFalse ? false :
BrowserSettings.WaitOnDocumentReady;
918 BrowserSettings.NetworkIdleTimeoutMs = networkIdleTimeoutMs;
942 BrowserSettings.NetworkIdleMaxConnections = maxConnections;
968 BrowserSettings.NetworkIdlePruneMs = networkIdlePruneMs;
993 BrowserSettings.ProfileDataDirectory = tmpProfileDirectory;
995 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Using profile data directory [{tmpProfileDirectory}].",
this, GPALObjectType.Browser);
1019 BrowserSettings.ProfileUserName = profileUserName;
1020 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Using profile user name [{profileUserName}].",
this, GPALObjectType.Browser);
1044 BrowserSettings.ProfileName = profileName;
1045 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Using profile name [{profileName}].",
this, GPALObjectType.Browser);
1058 BrowserSettings.StealthType = steathType;
1069 BrowserSettings.Referrer = referrer;
1080 BrowserSettings.OverrideUserAgent = userAgent;
1096 BrowserSettings.UserAgentFromBrowser = userAgentFromBrowser;
1126 selector.ElementsFoundAndMatchedCount = 0;
1127 selector.DeleteMe =
false;
1128 selector.RemovedMethods.Clear();
1129 if (
true == CurrentUOW.ActionCalled)
1132 var iframeRoot = CurrentUOW.ContextPath;
1133 var shadowRoot = CurrentUOW.ShadowRoot;
1137 ContextPath = iframeRoot,
1138 ShadowRoot = shadowRoot
1141 CurrentUOW.Browser =
this;
1142 selector.Browser =
this;
1145 ResolveInteractionType(selector);
1148 selector.WebSelectorFoundResults =
null;
1149 selector.WebSelectorMatchedResults =
null;
1155 BrowserSettings.CurrentSelector = selector;
1167 if (
true == CurrentUOW.ActionCalled)
1170 var iframeRoot = CurrentUOW.ContextPath;
1171 var shadowRoot = CurrentUOW.ShadowRoot;
1176 ContextPath = iframeRoot,
1177 ShadowRoot = shadowRoot
1182 CurrentUOW.Browser =
this;
1183 persistentUOW.Browser =
this;
1187 tmpSel.selectorSettings.SelectorType = SelectorType.Data;
1188 tmpSel.selectorSettings.Name =
"Literal" + CurrentUOW.WithSelectorList.Select(s => SelectorType.Data == s.SelectorSettings.SelectorType).Count() + 1;
1189 CurrentUOW.WithSelectorList.Add(tmpSel);
1200 tmpSel.selectorSettings.SelectorType = SelectorType.DataFunc;
1201 tmpSel.selectorSettings.Name = dataFunction.Method.Name;
1202 tmpSel.selectorSettings.DataFunction = dataFunction;
1203 CurrentUOW.WithSelectorList.Add(tmpSel);
1214 private int WalkInElementIfAny(Action body)
1216 UnitOfWork.WebElementWithType inElement = CurrentUOW.ContextPath?
1217 .LastOrDefault(context => ElementType.Element == context.elementType &&
null != context.selector);
1219 if (
null == inElement)
1228 for (
int container = 0; container < inElement.elements.Count; container++)
1230 inElement.current = container;
1235 inElement.current = 0;
1237 return inElement.elements.Count;
1247 private void RefreshContainers(UnitOfWork.WebElementWithType inElement)
1249 int at = CurrentUOW.ContextPath.IndexOf(inElement);
1251 CurrentUOW.ContextPath.RemoveAt(at);
1256 List<GPALElement> containers =
null;
1257 Stopwatch waited = Stopwatch.StartNew();
1258 long waitFor = WaitTime.Never < CurrentUOW.WaitForInMs ? (long)CurrentUOW.WaitForInMs : 0;
1262 ElementHelper.FindWebElements(
this, CurrentUOW, inElement.selector, out _, out containers,
null,
false);
1264 if (0 < containers?.Count)
break;
1266 if (waitFor > waited.ElapsedMilliseconds)
1267 System.Threading.Thread.Sleep(250);
1269 while (waitFor > waited.ElapsedMilliseconds);
1271 CurrentUOW.ContextPath.Insert(at, inElement);
1273 if (0 < containers?.Count)
1278 int wanted =
int.MaxValue;
1280 if (
int.MaxValue != CurrentUOW.WithAllThatMatch && 0 < CurrentUOW.WithAllThatMatch)
1281 wanted = CurrentUOW.WithAllThatMatch;
1284 else if (0 < GPAL.GPALSettings.WithAllThatMatch)
1285 wanted = GPAL.GPALSettings.WithAllThatMatch;
1287 if (
int.MaxValue != wanted && wanted < containers.Count)
1289 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{wanted}] containers kept of [{containers.Count}] found for selector [{inElement.selector.Name}] with [WithAllThatMatch({wanted})]",
this, GPALObjectType.Browser);
1291 containers = containers.Take(wanted).ToList();
1294 inElement.elements = containers;
1295 inElement.gPalElement = containers[0];
1299 inElement.elements =
null;
1300 inElement.gPalElement =
null;
1302 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"InElement selector returned no results [{inElement.selector.Name}]",
this, GPALObjectType.Browser);
1305 inElement.current = 0;
1317 private bool RowsFromContainers(List<ReadOnlyCollection<GPALElement>> tmpColumns,
1318 List<(
string Name,
string AttributeName)> selectorMeta,
int pages)
1320 bool retVal =
false;
1322 UnitOfWork.WebElementWithType inElement = CurrentUOW.ContextPath?
1323 .LastOrDefault(context => ElementType.Element == context.elementType &&
null != context.selector);
1325 if (
null != inElement)
1327 RefreshContainers(inElement);
1335 if (0 < inElement?.elements?.Count)
1337 List<Selector> selectors = CurrentUOW.WithSelectorList;
1338 List<List<GPALElement>> building = selectors.Select(sel =>
new List<GPALElement>()).ToList();
1340 foreach (Selector sel
in selectors)
1342 sel.WebSelectorFoundResults =
null;
1343 sel.WebSelectorMatchedResults =
null;
1345 selectorMeta.Add((sel.
Name, sel.AttributeName));
1348 int rows = WalkInElementIfAny(() =>
1350 for (
int column = 0; column < selectors.Count; column++)
1352 Selector sel = selectors[column];
1353 GPALElement cell =
null;
1355 if (SelectorType.Selector == sel.selectorSettings.
SelectorType)
1357 ElementHelper.FindWebElements(
this, CurrentUOW, sel, out _, out List<GPALElement> matchedElems);
1361 if (0 < matchedElems?.Count)
1363 cell = matchedElems[0];
1364 cell.AttributeName = sel.AttributeName;
1367 else if (SelectorType.Data == sel.selectorSettings.
SelectorType)
1369 cell = GPAL.Element;
1372 else if (SelectorType.DataFunc == sel.selectorSettings.
SelectorType)
1374 cell = GPAL.Element;
1375 cell.Text = sel.selectorSettings.DataFunction();
1380 cell =
new GPALElement(
new Point(0, 0),
new Size(1, 1), $
"{GPAL.ErrorPlaceholder} : {sel.Name}",
BrowserDriver,
"GPALElement")
1382 AttributeName = sel.AttributeName,
1387 building[column].Add(cell);
1391 foreach (List<GPALElement> column
in building)
1392 tmpColumns.Add(
new ReadOnlyCollection<GPALElement>(column));
1394 CurrentUOW.RowCount = rows;
1395 CurrentUOW.ColCount = selectors.Count;
1397 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Grid page [{pages + 1}] read [{rows}] containers of [{selectors.Count}] columns",
this, GPALObjectType.Browser);
1399 else if (
true == retVal)
1400 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No containers for [{inElement.selector.Name}] on page [{pages + 1}], so no rows from it. The page was not read another way",
this, GPALObjectType.Browser);
1422 if (
true ==
string.IsNullOrEmpty(origin))
1423 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No page loaded, so there is no session to carry. Navigate first, then continue as a RESTClient",
this, GPALObjectType.Browser);
1429 cookieUrl.
Add(
new StorageAction { StorageType = WebsiteStorageType.cookie, Action = WebsiteStorageAction.get });
1432 new StorageAction { StorageType = WebsiteStorageType.cookie, Action = WebsiteStorageAction.get },
1433 out
string cookieJson);
1435 string cookieHeader = CookieHeaderFrom(cookieJson, origin, out
int cookiesRead);
1437 if (
false ==
string.IsNullOrEmpty(cookieHeader))
1443 string sentAcceptLanguage =
BrowserHelper.LiveAcceptLanguage(
this);
1447 if (
false ==
string.IsNullOrEmpty(sentAcceptLanguage))
1448 retVal.
WithHeader(
"Accept-Language", sentAcceptLanguage);
1455 if (
null != browserCredential)
1460 if (
false ==
string.IsNullOrEmpty(authValue))
1465 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Continuing as a RESTClient from [{BrowserSettings.BrowserType}], carrying its [{(null == sentUserAgent ? "configured
" : "live
")}] user agent, [{(string.IsNullOrEmpty(sentAcceptLanguage) ? "no accept language
" : sentAcceptLanguage)}], [{cookieHeader?.Split(';').Length ?? 0}] of [{cookiesRead}] cookies for [{origin}] and [{(null == browserCredential ? "no credential
" : ((Credentials)browserCredential).WebAuthType.ToString())}]",
this, GPALObjectType.Browser);
1480 private static string CookieHeaderFrom(
string cookieJson,
string origin, out
int cookiesRead)
1482 string retVal =
null;
1488 if (
false ==
string.IsNullOrEmpty(cookieJson))
1490 string host =
true == Uri.TryCreate(origin, UriKind.Absolute, out Uri originUri) ? originUri.Host :
null;
1491 Newtonsoft.Json.Linq.JArray cookies = Newtonsoft.Json.Linq.JArray.Parse(cookieJson);
1492 List<string> pairs =
new List<string>();
1494 cookiesRead = cookies.Count;
1496 foreach (Newtonsoft.Json.Linq.JToken cookie in cookies)
1499 string name = (string)(cookie[
"name"] ?? cookie[
"Name"] ?? cookie[
"Key"]);
1500 string value = (string)(cookie[
"value"] ?? cookie[
"Value"]);
1501 string domain = (string)(cookie[
"domain"] ?? cookie[
"Domain"]);
1503 if (
false ==
string.IsNullOrEmpty(name) &&
true == UrlHelper.CookieAppliesToHost(domain, host))
1504 pairs.Add($
"{name}={value}");
1507 if (0 < pairs.Count)
1508 retVal =
string.Join(
"; ", pairs);
1511 catch (GPALException)
1515 catch (Exception ex)
1517 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Could not read the cookies into a header",
null, GPALObjectType.Browser, ex);
1532 BrowserSettings.StorageActionCalled =
true;
1533 BrowserSettings.StorageActionToRun.Action = WebsiteStorageAction.get;
1534 BrowserSettings.StorageActionToRun.StorageType = storageType;
1546 BrowserSettings.StorageActionCalled =
true;
1547 BrowserSettings.StorageActionToRun.Action = WebsiteStorageAction.set;
1548 BrowserSettings.StorageActionToRun.StorageType = storageType;
1560 BrowserSettings.StorageActionCalled =
true;
1561 BrowserSettings.StorageActionToRun.Action = WebsiteStorageAction.delete;
1562 BrowserSettings.StorageActionToRun.StorageType = storageType;
1578 BrowserSettings.StorageActionCalled =
false;
1581 BrowserSettings.StorageActionToRun.Domain = domain;
1597 BrowserSettings.StorageActionCalled =
false;
1600 BrowserSettings.StorageActionToRun.Path = path;
1616 BrowserSettings.StorageActionCalled =
false;
1619 BrowserSettings.StorageActionToRun.Key = key;
1635 BrowserSettings.StorageActionCalled =
false;
1638 BrowserSettings.StorageActionToRun.StoreName = storeName;
1654 BrowserSettings.StorageActionCalled =
false;
1657 BrowserSettings.StorageActionToRun.Data = data;
1673 BrowserSettings.StorageActionCalled =
false;
1676 BrowserSettings.StorageActionToRun.UserDefined = userDefined;
1703 selector.ElementsFoundAndMatchedCount = 0;
1704 selector.WebSelectorFoundResults =
null;
1705 selector.WebSelectorMatchedResults =
null;
1708 if (CurrentUOW.ActionCalled)
1710 var previousShadowRoot = CurrentUOW.ShadowRoot;
1711 var previousContextPath = CurrentUOW.ContextPath;
1716 ShadowRoot = previousShadowRoot,
1717 ContextPath = previousContextPath
1720 persistentUOW =
new UnitOfWork { Browser =
this };
1723 BrowserSettings.CurrentSelector = selector;
1724 CurrentUOW.InSelectorList.Add(selector);
1737 stepsToProcess = CurrentUOW.ContextPath;
1743 if (ElementType.IFrame == contextType.elementType)
1748 BrowserDriver.SwitchTo().Frame((IWebElement)contextType.gPalElement.WebElement);
1754 else if (ElementType.ShadowRoot == contextType.elementType)
1757 MagicHelper.SwitchToShadowRoot(contextType.gPalElement.Css);
1759 PuppeteerClient.SwitchToShadowRoot(contextType.gPalElement.Css).Execute();
1763 inElement = contextType.gPalElement;
1765 else if (ElementType.Element == contextType.elementType)
1767 inElement = contextType.gPalElement;
1777 CurrentUOW.ContextPath.Add(elementEntry);
1809 selector.ElementsFoundAndMatchedCount = 0;
1810 selector.WebSelectorFoundResults =
null;
1811 selector.WebSelectorMatchedResults =
null;
1814 if (CurrentUOW.ActionCalled)
1816 var previousShadowRoot = CurrentUOW.ShadowRoot;
1817 var previousContextPath = CurrentUOW.ContextPath;
1822 ShadowRoot = previousShadowRoot,
1823 ContextPath = previousContextPath
1826 persistentUOW =
new UnitOfWork { Browser =
this };
1829 BrowserSettings.CurrentSelector = selector;
1830 CurrentUOW.InSelectorList.Add(selector);
1843 stepsToProcess = CurrentUOW.ContextPath;
1848 if (ElementType.IFrame == contextType.elementType)
1851 MagicHelper.SwitchToElement(contextType.gPalElement.Css);
1855 BrowserDriver.SwitchTo().Frame((IWebElement)contextType.gPalElement.WebElement);
1861 else if (ElementType.ShadowRoot == contextType.elementType)
1864 MagicHelper.SwitchToShadowRoot(contextType.gPalElement.Css);
1866 PuppeteerClient.SwitchToShadowRoot(contextType.gPalElement.Css).Execute();
1870 inElement = contextType.gPalElement;
1875 ElementHelper.FindWebElements(
1876 this, CurrentUOW, selector, out _, out var tempElems, CurrentUOW.ShadowRoot);
1878 if (tempElems?.Count > 0)
1881 selector, tempElems[0], ElementType.IFrame);
1884 CurrentUOW.ContextPath.Add(iframeElement);
1922 shadowDomSelector.ElementsFoundAndMatchedCount = 0;
1923 shadowDomSelector.WebSelectorFoundResults =
null;
1924 shadowDomSelector.WebSelectorMatchedResults =
null;
1926 if (CurrentUOW.ActionCalled)
1928 var previousShadowRoot = CurrentUOW.ShadowRoot;
1929 var previousContextPath = CurrentUOW.ContextPath;
1934 ShadowRoot = previousShadowRoot,
1935 ContextPath = previousContextPath
1938 persistentUOW =
new UnitOfWork { Browser =
this };
1941 BrowserSettings.CurrentSelector = shadowDomSelector;
1942 CurrentUOW.InSelectorList.Add(shadowDomSelector);
1956 if (ElementType.IFrame == contextType.elementType)
1963 BrowserDriver.SwitchTo().Frame((IWebElement)contextType.gPalElement.WebElement);
1969 else if (ElementType.ShadowRoot == contextType.elementType)
1972 MagicHelper.SwitchToShadowRoot(contextType.gPalElement.Css);
1974 PuppeteerClient.SwitchToShadowRoot(contextType.gPalElement.Css).Execute();
1978 inElement = contextType.gPalElement;
1982 ElementHelper.FindWebElements(
1983 this, CurrentUOW, shadowDomSelector, out _, out var tempElems, inElement);
1985 if (tempElems?.Count > 0)
1988 shadowDomSelector, tempElems[0], ElementType.ShadowRoot);
1990 CurrentUOW.ShadowRoot = tempElems[0];
1991 CurrentUOW.ShadowRoot.IsShadowRoot =
true;
1994 CurrentUOW.ContextPath.Add(shadowElement);
2009 ISearchContext shadowRoot =
null;
2018 shadowRoot = ((IWebElement)tempElems[0].WebElement).GetShadowRoot();
2025 if (
null != shadowRoot)
2026 CurrentUOW.ShadowRoot.WebElement = shadowRoot;
2029 CurrentUOW.ShadowRoot.IsClosedShadowRoot =
true;
2034 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Closed shadow root for selector [{shadowDomSelector.Name}]. Will return host element for searches inside.",
this, GPALObjectType.Browser);
2048 CurrentUOW.ShadowRoot =
null;
2049 CurrentUOW.ContextPath?.Clear();
2074 CurrentUOW.NextPageButton = nextPageButtonselector;
2075 BrowserSettings.CurrentSelector = nextPageButtonselector;
2086 CurrentUOW.PageCount = pageCount;
2098 CurrentUOW.FetchTokens = inputGrid;
2105 List<IGPALGrid<string>> tokenList =
FileHelper.TokenizeFile(CurrentUOW, inputFile);
2107 CurrentUOW.FetchTokens = 0 == tokenList?.Count ? null : tokenList[0];
2114 DatabaseHelper.TokenizeDatabase(CurrentUOW, inputDatabase);
2116 CurrentUOW.FetchTokens = CurrentUOW.InputDatabase?.Tokens;
2123 IGPALGrid<string> grid =
GPAL.GridForType<
string>();
2125 grid.AddRow(
new List<string>(tokens?.Split(
',') ??
new string[0]));
2126 CurrentUOW.FetchTokens = grid;
2138 CurrentUOW.CallAfterFillIn = callAfterFillIn;
2154 CurrentUOW.CallIfFound.Add(callIfFoundDelegate);
2170 CurrentUOW.CallIfNotFound.Add(callIfNotFound);
2188 private HWND WaitForCommonDialog(
int waitTimeInSeconds)
2192 return FormHelper.WaitForCommonDialog(owner, waitTimeInSeconds);
2202 BrowserSettings.WaitForWindowTimeoutInSeconds = waitTimeInSeconds;
2216 persistentUOW.CallIfFound.Add(persistentCallIfFound);
2231 persistentUOW.CallIfNotFound.Add(persistentCallIfNotFound);
2259 BrowserSettings.Credentials = credentials;
2278 internal CallIfStatus RaiseOnFail(GPALFailure failure,
string detail)
2280 CallIfStatus handled = CallIfStatus.NotHandled;
2281 List<CallOnFailDelegate> handlers = 0 < BrowserSettings.CallOnFail.Count ? BrowserSettings.CallOnFail :
GPAL.GPALSettings.CallOnFailHandlers;
2285 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Invoking CallOnFail [{handler.Method.Name}] for [{failure}]",
this, GPALObjectType.Browser);
2287 handled = handler(
this, failure, detail);
2289 if (CallIfStatus.Terminate == handled)
2291 string str = $
"CallOnFail handler [{handler.Method.Name}] requested program termination.";
2296 if (CallIfStatus.Handled == handled)
2315 if (
true == CurrentUOW.ActionCalled)
2318 var iframeRoot = CurrentUOW.ContextPath;
2319 var shadowRoot = CurrentUOW.ShadowRoot;
2323 ContextPath = iframeRoot,
2324 ShadowRoot = shadowRoot,
2328 persistentUOW =
new UnitOfWork() { Browser =
this };
2331 selector.Browser =
this;
2332 selector.DeleteMe =
false;
2333 selector.RemovedMethods.Clear();
2335 ResolveInteractionType(selector);
2338 selector.WebSelectorFoundResults =
null;
2339 selector.WebSelectorMatchedResults =
null;
2341 persistentUOW.WithSelectorList.Add(selector);
2353 if (
true == CurrentUOW.GetGridCalled)
2355 CurrentUOW.GetGridCalled =
false;
2356 CurrentUOW.HeaderList.Clear();
2358 CurrentUOW.HeaderList.Add(header);
2374 if (0 == CurrentUOW.WithSelectorList.Count ||
true == CurrentUOW.ActionCalled)
2375 BrowserSettings.WithAllThatMatch = rowCount;
2377 CurrentUOW.WithAllThatMatch = rowCount;
2389 int screenWidth =
Puppeteer.GetScreenWidthInt();
2390 int screenHeight =
Puppeteer.GetScreenHeightInt();
2392 if (100 > windowSize.Width || 100 > windowSize.Height)
2394 int widthToUse = Math.Min(windowSize.Width, 100);
2395 int heightToUse = Math.Min(windowSize.Height, 100);
2397 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Requested browser sizes [{windowSize.Width}x{windowSize.Height}] cannot be less than 100. Using [{widthToUse}x{heightToUse}].",
this, GPALObjectType.Browser);
2399 windowSize.Width = widthToUse;
2400 windowSize.Height = heightToUse;
2402 else if (screenWidth < windowSize.Width || screenHeight < windowSize.Height)
2404 int widthToUse = Math.Min(windowSize.Width, screenWidth);
2405 int heightToUse = Math.Min(windowSize.Height, screenHeight);
2407 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Requested browser size [{windowSize.Width}x{windowSize.Height}] exceeds screen size [{screenWidth}x{screenHeight}]. Using [{widthToUse}x{heightToUse}].",
this, GPALObjectType.Browser);
2409 windowSize.Width = widthToUse;
2410 windowSize.Height = heightToUse;
2414 BrowserSettings.WindowSize = windowSize;
2455 CurrentUOW.InfiniteScroll =
true;
2459 #region ACTION GETTERS
2468 InAction(
false,
false);
2489 InAction(
false,
false);
2510 InAction(
false,
false);
2537 InAction(
false,
false);
2559 InAction(
false,
false);
2566 ScrollToPosition(
"end");
2581 InAction(
false,
false);
2588 ScrollToPosition(
"top");
2603 InAction(
false,
false);
2622 BrowserSettings.FullScreen =
false;
2623 BrowserSettings.Maximize =
false;
2624 BrowserSettings.Minimize =
false;
2657 BrowserSettings.Minimize =
false;
2658 BrowserSettings.Maximize =
false;
2659 BrowserSettings.FullScreen =
true;
2666 BrowserSettings.SavedWindowSize =
new Size(
MagicHelper.WindowOuterWidth(),
MagicHelper.WindowOuterHeight());
2671 BrowserSettings.SavedWindowSize =
new Size(
PuppeteerClient.WindowOuterWidth().Execute<
int>(),
PuppeteerClient.WindowOuterHeight().Execute<
int>());
2676 BrowserSettings.SavedWindowSize =
BrowserSettings.BrowserDriver.Manage().Window.Size;
2690 BrowserSettings.Maximize =
true;
2691 BrowserSettings.Minimize =
false;
2692 BrowserSettings.FullScreen =
false;
2700 BrowserSettings.SavedWindowSize =
new Size(
MagicHelper.WindowOuterWidth(),
MagicHelper.WindowOuterHeight());
2706 BrowserSettings.SavedWindowSize =
new Size(
PuppeteerClient.WindowOuterWidth().Execute<
int>(),
PuppeteerClient.WindowOuterHeight().Execute<
int>());
2712 BrowserSettings.SavedWindowSize =
BrowserSettings.BrowserDriver.Manage().Window.Size;
2726 BrowserSettings.Minimize =
true;
2727 BrowserSettings.Maximize =
false;
2728 BrowserSettings.FullScreen =
false;
2735 BrowserSettings.SavedWindowSize =
new Size(
MagicHelper.WindowOuterWidth(),
MagicHelper.WindowOuterHeight());
2740 BrowserSettings.SavedWindowSize =
new Size(
PuppeteerClient.WindowOuterWidth().Execute<
int>(),
PuppeteerClient.WindowOuterHeight().Execute<
int>());
2746 BrowserSettings.SavedWindowSize =
BrowserSettings.BrowserDriver.Manage().Window.Size;
2753 #endregion ACTION GETTERS
2754 #endregion <Browser Settings>
2755 #region <Browser Actions>
2763 BrowserSettings.ObeyRobotsTxt = trueFalse;
2773 BrowserSettings.RespectRobotMetaTags = trueFalse;
2787 BrowserSettings.UseHeadless =
true;
2790 BrowserSettings.OpenPDFExternally =
false;
2791 BrowserSettings.PromptForDownload =
false;
2793 BrowserSettings.OpenInTab =
false;
2800 catch (Exception ex)
2802 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to GET from [{URL}]",
this, GPALObjectType.Browser, ex);
2817 bool firstRunSave = firstRun;
2827 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Firefox cannot be driven with [{BrowserSettings.AutomationEngine}], so this browser runs on [{steppedDownTo}]",
this, GPALObjectType.Browser);
2836 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Visiting [{URL?.Url}] is disallowed by robots.txt and your request to honor it. Not going to URL. Workflow will fail.",
this, GPALObjectType.Browser);
2837 URL.
ForUrl(
"https://google.com");
2845 string priorHandle =
null;
2846 int tabsBefore = -1;
2848 BrowserSettings.CurrentURL = URL?.
Url;
2849 BrowserSettings.CurrentGPALUrl = URL;
2852 CurrentUOW =
new UnitOfWork() { Browser =
this };
2855 if (
true == InAction(
false,
false,
null))
2860 ApplyNetworkMonitor();
2862 CheckAndApplyStealth();
2884 UpdateActiveTab(tabTuple);
2900 if (
null == BrowserLaunched)
2902 BrowserLaunched = DateTime.Now;
2909 tabsBefore = RealTabCount();
2910 priorHandle =
true ==
UseSelenium ? BrowserDriver.CurrentWindowHandle :
null;
2915 if (
true ==
string.IsNullOrEmpty(tabTuple?.Url))
2930 RecordTabWindow(
BrowserDriver.CurrentWindowHandle, priorHandle);
2945 RaiseOnFail(GPALFailure.Navigation, $
"[{URL?.Url}] did not load.");
2947 RaiseOnFail(GPALFailure.Navigation, $
"[{URL?.Url}] did not load. [{PuppeteerCommunicator.LastNavigationError}]");
2952 if (-1 != tabsBefore && RealTabCount() == tabsBefore)
2953 BackOutOfSelfClosedTab(URL, priorUrl, priorGPALUrl, priorTab, priorHandle);
2959 if (
true ==
BrowserSettings.RespectRobotMetaTags &&
false == UrlHelper.CheckRobotMetaTags(URL,
this))
2961 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Visiting [{BrowserSettings.CurrentURL}] is disallowed by robot meta tags and your request to honor them..");
2962 URL.
ForUrl(
"https://google.com");
2968 UpdateActiveTab(tabTuple);
2983 BrowserSettings.CurrentURL = URL.
Url;
2987 if (
true == navigationFailed)
2988 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{BrowserSettings.CurrentURL}] did not load. Continuing against whatever the browser is showing.");
3004 catch (Exception ex)
3006 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to navigate to [{URL?.Url}]",
this, GPALObjectType.Browser, ex);
3014 BrowserSettings.OpenInTab =
false;
3017 throw new GPALException(
"Driver initialization error. Please see mesages.");
3025 private void ApplyGoogleReferrer()
3027 bool applied =
true;
3034 ((OpenQA.Selenium.Chromium.ChromiumDriver)
BrowserDriver).ExecuteCdpCommand(
"Network.setExtraHTTPHeaders",
new Dictionary<string, object>
3037 "headers",
new Dictionary<string, object>
3039 {
"Referer",
"https://www.google.com/" }
3046 if (
true == applied)
3047 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Stealth Google Referrer applied.",
this, GPALObjectType.Browser);
3049 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{BrowserSettings.BrowserType}] on [{BrowserSettings.AutomationEngine}] cannot set a header, so the Google referrer was not applied",
this, GPALObjectType.Browser);
3055 private void ApplyNetworkMonitor()
3057 string networkMonitorScript =
@"
3059 if (!window.__gpalNetworkMonitor) {
3060 window.__gpalNetworkMonitor = { inflight: 0 };
3062 const origOpen = XMLHttpRequest.prototype.open;
3063 XMLHttpRequest.prototype.open = function() {
3064 this.addEventListener('loadstart', () => { window.__gpalNetworkMonitor.inflight++; });
3065 this.addEventListener('loadend', () => { window.__gpalNetworkMonitor.inflight--; });
3066 origOpen.apply(this, arguments);
3069 const origFetch = window.fetch;
3070 window.fetch = function(...args) {
3071 window.__gpalNetworkMonitor.inflight++;
3072 return origFetch(...args).finally(() => { window.__gpalNetworkMonitor.inflight--; });
3081 .GetAwaiter().GetResult();
3087 ((OpenQA.Selenium.IJavaScriptExecutor)
BrowserDriver).ExecuteScript(networkMonitorScript);
3091 ((OpenQA.Selenium.Chromium.ChromiumDriver)
BrowserDriver).ExecuteCdpCommand(
3092 "Page.addScriptToEvaluateOnNewDocument",
3093 new Dictionary<string, object>
3095 {
"source", networkMonitorScript }
3100 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Network idle monitor script injected.",
this, GPALObjectType.Browser);
3106 private void ApplyCDPProtection()
3112 string antiAntiBotScript =
@"
3114 var originalError = Error;
3115 Object.defineProperty(Error.prototype, 'stack', {
3116 configurable: false,
3119 value: (function() {
3121 throw new originalError();
3128 window.Error = new Proxy(originalError, {
3129 construct(target, args) {
3130 var instance = new target(...args);
3131 return Object.freeze(instance);
3143 ((OpenQA.Selenium.Chromium.ChromiumDriver)
BrowserDriver).ExecuteCdpCommand(
"Page.addScriptToEvaluateOnNewDocument",
new Dictionary<string, object>
3145 {
"source", antiAntiBotScript }
3149 string antiAntiBotScript2 =
"Object.defineProperty(navigator, 'webdriver', { get: () => undefined })";
3152 ((OpenQA.Selenium.Chromium.ChromiumDriver)
BrowserDriver).ExecuteCdpCommand(
"Page.addScriptToEvaluateOnNewDocument",
new Dictionary<string, object>
3154 {
"source", antiAntiBotScript2 }
3159 PuppeteerCommunicator.EnableFetchScriptInjection(antiAntiBotScript2,
null).GetAwaiter().GetResult();
3165 if (
true == applied)
3166 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Stealth CDP protection applied.",
this, GPALObjectType.Browser);
3168 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{BrowserSettings.BrowserType}] on [{BrowserSettings.AutomationEngine}] cannot run a script on every new document, so CDP protection was not applied",
this, GPALObjectType.Browser);
3176 private void ApplyToStringOverride()
3178 string toStringOverrideScript =
@"
3180 // Store the original toString
3181 const originalToString = Object.prototype.toString;
3183 // Create a proxy handler to intercept toString calls
3185 apply: function(target, thisArg, argumentsList) {
3186 if (thisArg instanceof Function && /antiAntiBotScript|CDP/.test(originalToString.call(thisArg))) {
3187 return 'function () { [native code] }';
3189 return target.apply(thisArg, argumentsList);
3193 // Wrap toString with a proxy
3194 Object.prototype.toString = new Proxy(originalToString, handler);
3196 console.log('toString override applied via Proxy');
3202 ((OpenQA.Selenium.Chromium.ChromiumDriver)
BrowserDriver).ExecuteCdpCommand(
"Page.addScriptToEvaluateOnNewDocument",
new Dictionary<string, object>
3204 {
"source", toStringOverrideScript }
3209 PuppeteerCommunicator.EnableFetchScriptInjection(toStringOverrideScript,
null).GetAwaiter().GetResult();
3215 if (
true == applied)
3216 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Stealth toString override applied.",
this, GPALObjectType.Browser);
3218 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{BrowserSettings.BrowserType}] on [{BrowserSettings.AutomationEngine}] cannot run a script on every new document, so the toString override was not applied",
this, GPALObjectType.Browser);
3224 private void CheckAndApplyStealth()
3236 ApplyCDPProtection();
3238 if (
BrowserSettings.StealthType.HasFlag(StealthType.ToStringOverride))
3239 ApplyToStringOverride();
3247 ApplyGoogleReferrer();
3249 catch (GPALException)
3253 catch (Exception ex)
3255 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Google Referrer stealth skipped",
this, GPALObjectType.Browser, ex);
3259 BrowserSettings.StealthApplied =
true;
3271 if (URLorTabId is
string s)
3273 else if (URLorTabId is
GPALUrl url)
3275 else if (URLorTabId is
int i)
3284 int currentTabId = GetActiveTabId();
3287 if (
true ==
string.IsNullOrEmpty(URLorTabId))
3292 else if (URLorTabId is
int tabId)
3297 else if (URLorTabId is
string url)
3303 UpdateActiveTab(tabTuple);
3307 if (
true ==
string.IsNullOrWhiteSpace(URLorTabId))
3309 ResolveGhostTabHandle();
3313 string closing =
null;
3314 string landOn =
null;
3322 int closingIdx = before.IndexOf(closing);
3323 for (
int idx = closingIdx - 1; 0 <= idx &&
null == landOn; idx--)
3324 if (before[idx] != GhostTabHandle)
3325 landOn = before[idx];
3328 landOn = before.FirstOrDefault(h => h != closing && h != GhostTabHandle);
3330 catch (Exception exRead)
3332 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"CloseTab: could not read the tab list before closing, will fall back to whatever the driver leaves us on.",
this, GPALObjectType.Browser, exRead);
3338 catch (Exception exClose) {
GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"CloseTab: driver failed to close the current tab, continuing.",
this, GPALObjectType.Browser, exClose); }
3341 if (
false ==
string.IsNullOrEmpty(landOn))
3348 catch (Exception exSwitch)
3350 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"CloseTab: could not switch to tab [{landOn}] after closing.",
this, GPALObjectType.Browser, exSwitch);
3363 if (
true ==
string.IsNullOrEmpty(handle))
3364 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"CloseTab: no tab is on [{URLorTabId}], nothing closed. Call .CloseTab() with no url to close the current tab.",
this, GPALObjectType.Browser);
3371 catch (Exception exClose) {
GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"CloseTab: driver failed to close tab [{URLorTabId}], continuing.",
this, GPALObjectType.Browser, exClose); }
3376 string landOn =
true == after.Contains(staying) ? staying : after.LastOrDefault(h => h != GhostTabHandle);
3378 if (
false ==
string.IsNullOrEmpty(landOn))
3395 tabsLeft = RealTabCount();
3400 catch (Exception ex)
3402 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Browser stopped answering after closing the tab.",
this, GPALObjectType.Browser, ex);
3406 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Closed the last tab, the browser closed with it.",
this, GPALObjectType.Browser);
3424 private List<string> TabsInThisWindow(IList<string> handles,
string current)
3426 List<string> siblings =
new List<string>();
3430 if (
false == tabWindows.ContainsKey(current))
3431 tabWindows[current] = current;
3433 string thisWindow = tabWindows[current];
3435 foreach (
string handle
in handles)
3437 if (
false == tabWindows.ContainsKey(handle))
3438 tabWindows[handle] = thisWindow;
3440 if (tabWindows[handle] == thisWindow && handle != GhostTabHandle)
3441 siblings.Add(handle);
3452 private void RecordTabWindow(
string newHandle,
string openedFrom)
3454 if (
true ==
string.IsNullOrEmpty(newHandle))
3457 if (
false ==
string.IsNullOrEmpty(openedFrom) &&
true == tabWindows.ContainsKey(openedFrom))
3458 tabWindows[newHandle] = tabWindows[openedFrom];
3460 tabWindows[newHandle] =
string.IsNullOrEmpty(openedFrom) ? newHandle : openedFrom;
3469 internal string ResolveGhostTabHandle()
3473 return GhostTabHandle;
3476 if (
false ==
string.IsNullOrEmpty(GhostTabHandle))
3477 return GhostTabHandle;
3482 if (handles.Count <= tabsWeOpened)
3487 catch {
return null; }
3489 foreach (
string handle
in handles)
3491 if (handle == current)
continue;
3493 string handleUrl =
null;
3500 finally {
try {
BrowserDriver.SwitchTo().Window(current); }
catch { } }
3506 if (
false ==
string.IsNullOrEmpty(handleUrl) &&
true == UrlHelper.SameBaseUrl(handleUrl, GhostTabUrl))
3508 GhostTabHandle = handle;
3509 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Ghost tab found, will always skip [{handleUrl}]",
this, GPALObjectType.Browser);
3514 return GhostTabHandle;
3548 UpdateActiveTab(tabTuple);
3556 ResolveGhostTabHandle();
3564 List<string> siblings = TabsInThisWindow(handles, current);
3566 int idx = siblings.IndexOf(current);
3567 if (0 > idx) idx = 0;
3569 idx = (idx + 1) % siblings.Count;
3606 BrowserSettings.OpenInTab =
true;
3648 ResolveGhostTabHandle();
3654 List<string> siblings = TabsInThisWindow(handles, current);
3656 int idx = siblings.IndexOf(current);
3657 if (0 > idx) idx = 0;
3660 idx = (idx - 1 + siblings.Count) % siblings.Count;
3700 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
3701 ElementHelper.LeftClick(
this, modifierKeys);
3705 ResolveGhostTabHandle();
3716 foreach (
string handle
in handles.Where(h =>
false == handlesBefore.Contains(h) && h != GhostTabHandle))
3718 RecordTabWindow(handle, currentHandle);
3722 if (0 < opened) tabsWeOpened += opened;
3732 string currentUrl = GetSetCurrentUrl();
3736 int tabId = GetActiveTabId();
3738 UpdateActiveTab(tabTuple);
3756 if (
null != selector)
3770 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
3774 ElementHelper.GenericClick(
this, selectClickType);
3791 if (
null != selector)
3795 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
3801 ElementHelper.LeftClick(
this, trusted:
true);
3803 ElementHelper.LeftClick(
this);
3806 ((OpenQA.Selenium.Chromium.ChromiumDriver)
BrowserDriver).ExecuteCdpCommand(
"Runtime.disable",
new Dictionary<string, object> { });
3807 ElementHelper.LeftClick(
this);
3808 ((OpenQA.Selenium.Chromium.ChromiumDriver)
BrowserDriver).ExecuteCdpCommand(
"Runtime.enable",
new Dictionary<string, object> { });
3812 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{BrowserSettings.BrowserType}] on [{BrowserSettings.AutomationEngine}] cannot disable the runtime around a click, so this is an ordinary left click",
this, GPALObjectType.Browser);
3813 ElementHelper.LeftClick(
this);
3830 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
3832 ElementHelper.GenericClick(
this, ClickType.LeftDoubleClick);
3852 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
3853 ElementHelper.MiddleClick(
this);
3855 string currentUrl = GetSetCurrentUrl();
3859 ResolveGhostTabHandle();
3870 foreach (
string handle
in handles.Where(h =>
false == handlesBefore.Contains(h) && h != GhostTabHandle))
3872 RecordTabWindow(handle, currentHandle);
3876 if (0 < opened) tabsWeOpened += opened;
3879 if (
false == UrlHelper.AreEquivalent(priorUrl, currentUrl))
3883 int tabId = GetActiveTabId();
3885 UpdateActiveTab(tabTuple);
3903 if (
null != selector)
3907 selector.InteractionType = InteractionType.Selenium;
3911 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
3913 ElementHelper.GenericClick(
this, ClickType.RightClick);
3931 bool useEAvars =
false;
3944 EAelement = webElement;
3945 EAselector = selector;
3947 LeftClickAndDownload(gPalFile);
3951 internal const int UnexpectedSaveAsWaitSeconds = 2;
3952 internal const int ProfileRestoreWaitMs = 5000;
3962 string fullFilePathToSave;
3963 HWND windowOpened = IntPtr.Zero;
3964 List<string> returnFilename =
new List<string>();
3965 bool noSaveAsDialog =
false;
3969 if (
true == useEAvars)
3971 DownloadWorkflow(EAelement, EAselector);
3979 returnFilename.Add(fullFilePathToSave);
3981 string currentUrl =
null, currentUrl2 =
null;
3987 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Downloading to [{fullFilePathToSave}]",
this, GPALObjectType.Browser);
3994 string directUrl =
FileHelper.GetDownloadUrl(webElement);
3996 if (
true ==
string.IsNullOrWhiteSpace(directUrl))
4001 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[{webElement?.TagName}] carries no url to download, so WithUseDirectDownload has nothing to ask for. Leave it off and the click is used instead",
this, GPALObjectType.Browser);
4008 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[{directUrl}] did not download to [{fullFilePathToSave}]",
this, GPALObjectType.Browser);
4022 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Element [{webElement.TagName}] for selector [{selector.Name}] [{selector.InteractionType.ToString()}][Left] clicked with modifiers [NONE].");
4023 PuppeteerClient.LeftClickAndDownload(webElement.ElementHandle).DownloadTo(fileToSave).Execute();
4030 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Element [{webElement.TagName}] for selector [{selector.Name}] [{selector.InteractionType.ToString()}][Left] clicked with modifiers [NONE].");
4036 ElementHelper.Click(
this, selector, webElement, ClickType.LeftClick, ModifierKeys.NONE);
4046 if (
true == UrlHelper.AreEquivalent(currentUrl, urlAfterClick))
4047 CheckPersistentSelectors();
4049 if (
false == UrlHelper.AreEquivalent(currentUrl, urlAfterClick)
4050 &&
true == fileToSave.
Filename.EndsWith(
".pdf")
4064 HWND unexpectedDialog = WaitForCommonDialog(UnexpectedSaveAsWaitSeconds);
4066 if (IntPtr.Zero != unexpectedDialog)
4068 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{BrowserSettings.BrowserType}] asked where to save the file even though prompting was turned off. Answering the dialog with [{newFilename}].",
this, GPALObjectType.Browser);
4070 HardwareHelper.SendString(newFilename);
4082 Stopwatch sinceProgress = Stopwatch.StartNew();
4083 long lastSeen =
long.MinValue;
4091 long progress =
FileHelper.DownloadProgress(watchedDirectory, fullFilePathToSave);
4093 if (progress != lastSeen)
4095 lastSeen = progress;
4096 sinceProgress.Restart();
4110 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Testing [{currentUrl2}] for PDF file",
this, GPALObjectType.Browser);
4120 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"File not downloaded to [{fullFilePathToSave}].",
this, GPALObjectType.Browser);
4126 ElementHelper.Click(
this, selector, webElement, ClickType.LeftClick, ModifierKeys.NONE);
4134 if (
true == UrlHelper.AreEquivalent(currentUrl, urlAfterClick))
4135 CheckPersistentSelectors();
4141 if (
false == UrlHelper.AreEquivalent(currentUrl, urlAfterClick)
4142 &&
true == fileToSave.
Filename.EndsWith(
".pdf")
4150 windowOpened = WaitForCommonDialog(
BrowserSettings.WaitForWindowTimeoutInSeconds);
4156 noSaveAsDialog =
false;
4158 else if (IntPtr.Zero != windowOpened)
4160 HardwareHelper.SendString(newFilename);
4164 windowOpened = WaitForCommonDialog(1);
4166 if (IntPtr.Zero != windowOpened)
4169 string actualDownloadedFilename =
null;
4173 FileDownloaded =
true !=
string.IsNullOrEmpty(actualDownloadedFilename);
4176 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"File not downloaded to [{fullFilePathToSave}]",
this, GPALObjectType.Browser);
4185 noSaveAsDialog =
true;
4194 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Testing [{currentUrl2}] for PDF file",
this, GPALObjectType.Browser);
4207 if (
true == noSaveAsDialog)
4210 string downloadUrl =
FileHelper.GetDownloadUrl(webElement);
4211 string filename = Path.GetFileName(downloadUrl) ?? fullFilePathToSave;
4213 string wildcardFilename =
"*" + Path.GetExtension(filename);
4214 string systemDownloadedTo = Path.Combine(downloadDirectory, wildcardFilename);
4215 string actualDownloadedFilename =
null;
4217 actualDownloadedFilename =
FileHelper.WaitForDownloadToFinish(systemDownloadedTo,
BrowserSettings.DownloadTimeoutInSec);
4219 FileDownloaded =
true !=
string.IsNullOrEmpty(actualDownloadedFilename);
4223 if (
false == fullFilePathToSave.Equals(actualDownloadedFilename))
4225 File.Move(actualDownloadedFilename, fullFilePathToSave);
4226 if (
false == actualDownloadedFilename.Equals(fullFilePathToSave) &&
true == File.Exists(actualDownloadedFilename))
4227 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Downloaded [{actualDownloadedFilename}] unable to move to [{fullFilePathToSave}]",
null, GPALObjectType.None);
4229 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{actualDownloadedFilename}] moved to [{fullFilePathToSave}]",
this, GPALObjectType.Browser);
4233 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{filename}] not found in [{downloadDirectory}]",
this, GPALObjectType.Browser);
4241 FileHelper.watcher =
null;
4252 if (
int.MaxValue != CurrentUOW.WithAllThatMatch && ++rowCount == CurrentUOW.WithAllThatMatch)
4258 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4260 foreach (
Selector selector
in CurrentUOW.WithSelectorList)
4262 if (SelectorType.Selector != selector.SelectorType)
4265 ReadOnlyCollection<GPALElement> webelements = ElementHelper.FindWebElements(
this, CurrentUOW, selector, out
bool matchedAll, out List<GPALElement> matchedElements);
4266 if (
null != webelements)
4271 int matchCount = matchedElements.Count;
4273 for (
int elementIdx = 0; elementIdx < matchCount; elementIdx++)
4279 if (
false == UrlHelper.AreEquivalent(pageOfTheElements, urlNow))
4281 GoTo(pageOfTheElements);
4282 ElementHelper.FindWebElements(
this, CurrentUOW, selector, out
bool _, out matchedElements);
4284 if (matchedElements.Count != matchCount)
4285 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{selector.Name}] matched [{matchedElements.Count}] on the way back rather than [{matchCount}], so the remaining downloads may not be the ones first found.",
this, GPALObjectType.Browser);
4288 if (elementIdx >= matchedElements.Count)
4292 if (-1 == DownloadWorkflow(matchedElements[elementIdx], selector))
4298 foreach (
string filepath
in returnFilename)
4319 EAelement = webElement;
4320 EAselector = selector;
4322 LeftClickAndUpload(gPalFile);
4334 HWND windowOpened = IntPtr.Zero;
4336 if (
true == useEAvars)
4338 UploadWorkflow(EAelement, EAselector,
false);
4345 string fileToUpload =
null;
4353 && 1 < gPalFileToUpload.Count
4356 for (
int fileCount = 0; fileCount < gPalFileToUpload.
Count; fileCount++)
4358 fileToUpload = gPalFileToUpload.
Filenames[fileCount];
4360 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Uploading [{fileToUpload}]",
this, GPALObjectType.Browser);
4369 if (
true ==
string.IsNullOrEmpty(fileToUpload))
4370 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"For some reason we have an empty filename", gPalFileToUpload, GPALObjectType.Other);
4375 if (
true == uploadAllFiles)
4377 PuppeteerClient.LeftClickAndUpload(webElement.ElementHandle).UploadFrom(gPalFileToUpload).Execute();
4380 PuppeteerClient.LeftClickAndUpload(webElement.ElementHandle).UploadFrom(fileToUpload).Execute();
4389 string filesToSend =
true == sendAllAtOnce ?
string.Join(
"\n", gPalFileToUpload.
Filenames) : fileToUpload;
4393 ((IWebElement)webElement.iWebElement).SendKeys(filesToSend);
4397 ElementHelper.FindWebElements(
this, CurrentUOW, selector, out
bool _, out List<GPALElement> matchedElements);
4398 webElement = 0 < matchedElements.Count ? matchedElements[0] :
null;
4399 webElement?.
SendKeys(filesToSend.Replace(
"\\",
"/"));
4402 if (
true == sendAllAtOnce)
4410 ElementHelper.Click(
this, selector, webElement, ClickType.LeftClick, ModifierKeys.NONE);
4412 windowOpened = WaitForCommonDialog(
BrowserSettings.WaitForWindowTimeoutInSeconds);
4415 if (IntPtr.Zero != windowOpened)
4417 int filesToSend = 1;
4419 if (
true == webElement.
GetAttribute(
"multiple")?.ToLower().Equals(
"true"))
4420 filesToSend = gPalFileToUpload.
Count;
4422 if (
true == uploadAllFiles)
4423 for (
int idx = 0; idx < gPalFileToUpload.
Count; idx++)
4425 fileToUpload = gPalFileToUpload.
Filenames[idx];
4426 HardwareHelper.SendString(
$@"""{fileToUpload}"" ");
4429 HardwareHelper.SendString(
$@"""{fileToUpload}"" ");
4434 windowOpened = WaitForCommonDialog(1);
4436 if (IntPtr.Zero != windowOpened)
4439 if (
true == uploadAllFiles)
4459 if (
int.MaxValue != CurrentUOW.WithAllThatMatch && fileCount == CurrentUOW.WithAllThatMatch &&
false == uploadAllFiles)
4461 else if (
false == uploadAllFiles)
4468 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4470 foreach (
Selector selector
in CurrentUOW.WithSelectorList)
4472 if (SelectorType.Selector != selector.SelectorType)
4475 ReadOnlyCollection<GPALElement> webelements = ElementHelper.FindWebElements(
this, CurrentUOW, selector, out
bool matchedAll, out List<GPALElement> matchedElements);
4476 if (
null != webelements)
4478 foreach (
GPALElement webElement
in matchedElements)
4481 if (-1 == UploadWorkflow(webElement, selector, 1 == matchedElements.Count))
4504 string fullFilePathToSave;
4505 HWND windowOpened = IntPtr.Zero;
4506 List<string> returnFilename =
new List<string>();
4510 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4512 foreach (
Selector selector
in CurrentUOW.WithSelectorList)
4514 if (SelectorType.Selector != selector.SelectorType)
4517 ReadOnlyCollection<GPALElement> webelements = ElementHelper.FindWebElements(
this, CurrentUOW, selector, out
bool matchedAll, out List<GPALElement> matchedElements);
4518 if (
null != webelements)
4520 foreach (
GPALElement webElement
in matchedElements)
4525 returnFilename.Add(fullFilePathToSave);
4533 selector.InteractionType = InteractionType.JavaScript;
4535 selector.InteractionType = InteractionType.Hardware;
4540 bool noSaveAsDialog =
false;
4541 string currentUrl =
null, currentUrl2 =
null;
4550 ElementHelper.Click(
this, selector, webElement, ClickType.RightClick, ModifierKeys.NONE);
4574 selector.InteractionType = interactionType;
4576 windowOpened = WaitForCommonDialog(
BrowserSettings.WaitForWindowTimeoutInSeconds);
4582 if (IntPtr.Zero != windowOpened
4584 &&
"Save As" == FormHelper.TitleOf(windowOpened))
4585 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"GPAL thinks a misclick happened and the wrong item is saved. Please verify [{fullFilePathToSave}].",
this, GPALObjectType.Browser);
4587 if (IntPtr.Zero != windowOpened)
4589 HardwareHelper.SendString(fullFilePathToSave);
4593 windowOpened = WaitForCommonDialog(5);
4595 if (IntPtr.Zero != windowOpened)
4599 string actualDownloadedFilename2 =
null;
4601 actualDownloadedFilename2 =
FileHelper.WaitForDownloadToFinish(fullFilePathToSave,
BrowserSettings.DownloadTimeoutInSec);
4603 FileDownloaded =
true !=
string.IsNullOrEmpty(actualDownloadedFilename2);
4614 noSaveAsDialog =
true;
4628 if (
false ==
BrowserSettings.OpenPDFExternally &&
true == fullFilePathToSave.EndsWith(
".pdf"))
4630 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Testing [{currentUrl2}] for PDF file",
this, GPALObjectType.Browser);
4642 if (
true == noSaveAsDialog)
4645 string downloadUrl =
FileHelper.GetDownloadUrl(webElement);
4646 string filename = Path.GetFileName(downloadUrl) ?? fullFilePathToSave;
4648 string wildcardFilename =
"*" + Path.GetExtension(filename);
4649 string systemDownloadedTo = Path.Combine(downloadDirectory, wildcardFilename);
4650 string actualDownloadedFilename2 =
null;
4652 actualDownloadedFilename2 =
FileHelper.WaitForDownloadToFinish(systemDownloadedTo,
BrowserSettings.DownloadTimeoutInSec);
4654 FileDownloaded =
true !=
string.IsNullOrEmpty(actualDownloadedFilename2);
4658 if (
false == fullFilePathToSave.Equals(actualDownloadedFilename2))
4660 File.Move(actualDownloadedFilename2, fullFilePathToSave);
4661 if (
false == actualDownloadedFilename2.Equals(fullFilePathToSave) &&
true == File.Exists(actualDownloadedFilename2))
4662 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Downloaded file [{actualDownloadedFilename2}] unable to move to [{fullFilePathToSave}]",
null, GPALObjectType.None);
4664 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"[{actualDownloadedFilename2}] moved to [{fullFilePathToSave}]",
this, GPALObjectType.Browser);
4674 string rightClickUrl =
FileHelper.GetDownloadUrl(webElement);
4676 if (
true ==
string.IsNullOrWhiteSpace(rightClickUrl))
4679 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[{webElement?.TagName}] carries no url to download, so there is nothing to save",
this, GPALObjectType.Browser);
4686 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"[{rightClickUrl}] did not download to [{fullFilePathToSave}]",
this, GPALObjectType.Browser);
4720 if (
int.MaxValue != CurrentUOW.WithAllThatMatch && ++rowCount == CurrentUOW.WithAllThatMatch)
4726 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to detect Save As open window.",
this, GPALObjectType.Browser);
4730 foreach (
string filepath
in returnFilename)
4736 if (
true == File.Exists(filepath))
4737 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"File saved to [{filepath}]",
this, GPALObjectType.Browser);
4739 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Nothing was saved to [{filepath}]",
this, GPALObjectType.Browser);
4755 DatabaseHelper.TokenizeDatabase(CurrentUOW, inputDatabase);
4756 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4770 DatabaseHelper.TokenizeDatabase(CurrentUOW, inputDatabase);
4771 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4785 DatabaseHelper.TokenizeDatabase(CurrentUOW, inputDatabase);
4786 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4800 ((IGPALFileInternal)inputFile).TokenList =
FileHelper.TokenizeFile(CurrentUOW, inputFile);
4801 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4802 foreach (IGPALGrid<string> tokens
in ((IGPALFileInternal)inputFile).TokenList)
4816 ((IGPALFileInternal)inputFile).TokenList =
FileHelper.TokenizeFile(CurrentUOW, inputFile);
4817 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4818 foreach (IGPALGrid<string> tokens
in ((IGPALFileInternal)CurrentUOW.InputFile).TokenList)
4832 ((IGPALFileInternal)inputFile).TokenList =
FileHelper.TokenizeFile(CurrentUOW, inputFile);
4833 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4834 foreach (IGPALGrid<string> tokens
in ((IGPALFileInternal)inputFile).TokenList)
4848 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4862 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4876 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4888 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4889 ElementHelper.FillInFrom(
this, text, WriteMode.Append);
4901 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4902 ElementHelper.FillInFrom(
this, text, WriteMode.Overwrite);
4915 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4916 ElementHelper.FillInFrom(
this, textToUse, WriteMode.Insert);
4927 if (
null != selector)
4930 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4931 ElementHelper.Focus(
this);
4942 if (
null != selector)
4945 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4946 ElementHelper.Hide(
this);
4953 private string _pendingSetAttributeName;
4964 _pendingSetAttributeName = attribute;
4975 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4976 ElementHelper.
SetAttribute(
this, _pendingSetAttributeName, value);
4991 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
4992 ElementHelper.SetValueFromElement(
this, selector);
5021 if (
null != selector)
5025 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
5026 ElementHelper.MoveTo(
this);
5040 ReadOnlyCollection<GPALElement> tmpElems;
5042 if (
true == CurrentUOW.ActionCalled)
5044 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Action has already been called. This workflow has already run. Continuing.",
this, GPALObjectType.Browser);
5048 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Workflow starting, you should be careful if you follow this with actions. CallIf handlers will be invoked again.",
this, GPALObjectType.Browser);
5050 if (
true == WorkflowSetup(WaitTime.Never < CurrentUOW.WaitForInMs,
true))
5051 for (
int pages = 0; pages < CurrentUOW.PageCount; pages++)
5053 foreach (
Selector sel
in CurrentUOW.WithSelectorList)
5054 if (SelectorType.Selector == sel.SelectorType)
5055 tmpElems = ElementHelper.FindWebElements(
this, CurrentUOW, sel, out
bool matchedAll, out List<GPALElement> matchedElems);
5057 if (pages < CurrentUOW.PageCount - 1)
5063 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No next page, breaking loop. Retrieved [{pages + 1}] out of [{CurrentUOW.PageCount}] pages.",
this, GPALObjectType.Browser);
5073 elements =
new List<GPALElement>();
5077 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Action has already been called. Not retrieving elements.",
this, GPALObjectType.Browser);
5081 if (
true == WorkflowSetup(WaitTime.Never < CurrentUOW.WaitForInMs,
true))
5082 for (
int pages = 0; pages < CurrentUOW.
PageCount; pages++)
5085 if (SelectorType.Selector == sel.SelectorType)
5087 ElementHelper.FindWebElements(
this, CurrentUOW, sel, out
bool matchedAll, out List<GPALElement> matchedElems);
5088 if (sel.WebSelectorFoundResults !=
null)
5089 elements.AddRange(sel.WebSelectorFoundResults);
5094 BrowserHelper.TopBrowser(
this,
false);
5095 if (
false == BrowserHelper.GotoNextPage(
this))
5097 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No next page, breaking loop. Retrieved [{pages + 1}] out of [{CurrentUOW.PageCount}] pages.",
this, GPALObjectType.Browser);
5134 List<ReadOnlyCollection<GPALElement>> columns =
new List<ReadOnlyCollection<GPALElement>>();
5135 List<ReadOnlyCollection<GPALElement>> tmpColumns =
new List<ReadOnlyCollection<GPALElement>>();
5136 var selectorMeta =
new List<(string Name, string AttributeName)>();
5137 List<GPALElement> matchedElems =
new List<GPALElement>();
5138 dynamic foundElements =
null;
5144 CurrentUOW.ActionCalled =
true;
5145 CurrentUOW.GetGridCalled =
true;
5146 CurrentUOW.RetGrid.Clear();
5155 if (TryGetGridFromTable(ref returnGrid))
5161 bool useDedupe = (returnGrid as GPALGrid<string>)?.UseDedupe ??
false;
5162 var seenRows =
new HashSet<string>();
5163 if (
true == useDedupe &&
null != returnGrid)
5164 foreach (List<string> existingRow
in returnGrid)
5165 seenRows.Add(RowDedupeKey(existingRow));
5166 bool capReached =
false;
5168 for (
int pages = 0; pages < CurrentUOW.PageCount && !capReached; pages++)
5173 bool waitOnSelectors =
null == CurrentUOW.ContextPath?
5174 .LastOrDefault(context => ElementType.Element == context.elementType &&
null != context.selector);
5177 if (!InAction(
true == waitOnSelectors && WaitTime.Never < CurrentUOW.WaitForInMs))
5179 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"WaitFor did not find the elements for the grid. Not retrieving anything.",
this, GPALObjectType.Browser);
5185 CurrentUOW.MatchedRowIndexes.Clear();
5186 selectorMeta.Clear();
5194 if (
true == RowsFromContainers(tmpColumns, selectorMeta, pages))
5196 columns = tmpColumns;
5199 foreach (
Selector sel
in CurrentUOW.WithSelectorList)
5202 sel.WebSelectorFoundResults =
null;
5203 sel.WebSelectorMatchedResults =
null;
5205 selectorMeta.Add((sel.
Name, sel.AttributeName));
5208 if (SelectorType.Selector == sel.selectorSettings.
SelectorType)
5210 bool matchedAll =
false;
5212 foundElements = ElementHelper.FindWebElements(
this, CurrentUOW, sel, out matchedAll, out matchedElems);
5216 if (matchedElems?.Count > 0)
5219 element.AttributeName = sel.AttributeName;
5221 tmpColumns.Add(
new ReadOnlyCollection<GPALElement>(matchedElems));
5227 var placeholder =
new List<GPALElement>();
5228 int rows = CurrentUOW.RowCount > 0 ? CurrentUOW.RowCount : 0;
5231 for (
int i = 0; i < rows; i++)
5232 placeholder.Add(
new GPALElement(
new Point(0, 0),
new Size(1, 1), $
"{GPAL.ErrorPlaceholder} : {sel.Name}",
BrowserDriver,
"GPALElement") { AttributeName = sel.AttributeName, Browser =
this,
Selector = sel });
5233 tmpColumns.Add(
new ReadOnlyCollection<GPALElement>(placeholder));
5237 if (matchedElems?.Count > 0)
5239 CurrentUOW.ColCount++;
5243 CurrentUOW.RowCount = Math.Max(CurrentUOW.RowCount, matchedElems.Count);
5244 else if (matchedElems?.Count > 0)
5246 CurrentUOW.PartialMatch =
true;
5248 foreach (var elem
in matchedElems)
5249 if ((tmpIdx = matchedElems.IndexOf(elem)) != -1 && !CurrentUOW.MatchedRowIndexes.ContainsKey(tmpIdx))
5250 CurrentUOW.MatchedRowIndexes.Add(tmpIdx, sel.AttributeName);
5251 CurrentUOW.RowCount = CurrentUOW.MatchedRowIndexes.Count;
5254 CurrentUOW.RowCount = int.MaxValue != CurrentUOW.WithAllThatMatch ? CurrentUOW.WithAllThatMatch : 0;
5257 else if (SelectorType.Data == sel.selectorSettings.
SelectorType)
5259 CurrentUOW.ColCount++;
5262 tmpColumns.Add(
new ReadOnlyCollection<GPALElement>(
new List<GPALElement>(1) { tmpElement }));
5264 else if (SelectorType.DataFunc == sel.selectorSettings.
SelectorType)
5266 CurrentUOW.ColCount++;
5268 tmpElement.Text = sel.selectorSettings.DataFunction();
5269 tmpColumns.Add(
new ReadOnlyCollection<GPALElement>(
new List<GPALElement>(1) { tmpElement }));
5274 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Grid for [{sel.Name}] page [{pages + 1}] returned [{CurrentUOW.RowCount}] rows of [{tmpColumns.Count()}] columns",
this, GPALObjectType.Browser);
5277 if (CurrentUOW.PartialMatch)
5280 foreach (var colElems
in tmpColumns)
5282 var newCol =
new List<GPALElement>();
5288 foreach (var idx
in CurrentUOW.MatchedRowIndexes.Keys)
5289 if (idx < colElems.Count)
5290 newCol.Add(colElems[idx]);
5291 columns.Add(
new ReadOnlyCollection<GPALElement>(newCol));
5294 else if (0 == columns.Count)
5295 columns = tmpColumns;
5298 int pageRows = 0 < columns.Count ? columns.Max(c => c.Count) : 0;
5299 int targetRows = CurrentUOW.RowCount > 0 ? CurrentUOW.RowCount : pageRows;
5303 if (
int.MaxValue != CurrentUOW.WithAllThatMatch && CurrentUOW.WithAllThatMatch <= pageRows)
5304 targetRows = Math.Min(targetRows, CurrentUOW.WithAllThatMatch);
5308 for (
int i = 0; i < columns.Count; i++)
5310 var col = columns[i].ToList();
5311 while (col.Count < targetRows)
5314 var firstNonEmpty = columns.FirstOrDefault(c => c.Count > 0)?.FirstOrDefault();
5315 var meta = selectorMeta[i];
5316 col.Add(
new GPALElement(
new Point(0, 0),
new Size(1, 1), $
"{GPAL.ErrorPlaceholder} : {meta.Name}",
BrowserDriver,
"GPALElement") { AttributeName = meta.AttributeName, Browser =
this, Css = firstNonEmpty?.Css, ElementBackendNodeId = (int)(firstNonEmpty?.ElementBackendNodeId) });
5318 columns[i] =
new ReadOnlyCollection<GPALElement>(col);
5320 CurrentUOW.RowCount = targetRows;
5327 for (
int r = 0; r < CurrentUOW.RowCount && !capReached; r++)
5329 var row =
new List<string>();
5330 for (
int c = 0; c < columns.Count; c++)
5332 var elem = columns[c][r];
5337 if (
false ==
BrowserSettings.UseHeadless &&
false ==
"GPALElement".Equals(elem.TagName))
5340 if (
false ==
string.IsNullOrEmpty(elem.Css))
5346 ElementHelper.ScrollIntoView(
this, elem);
5348 if (!
string.IsNullOrEmpty(elem.AttributeName))
5349 row.Add(elem.GetAttribute(elem.AttributeName));
5350 else if (
null != elem.TagName)
5352 switch (elem.TagName?.ToLower())
5354 case "a": row.Add(elem.GetAttribute(
"href"));
break;
5355 case "img": row.Add(elem.GetAttribute(
"src"));
break;
5356 default: row.Add(elem.Text);
break;
5364 if (
true == useDedupe &&
false == seenRows.Add(RowDedupeKey(row)))
5367 CurrentUOW.RetGrid.AddRow(row);
5368 if (
int.MaxValue != CurrentUOW.WithAllThatMatch && CurrentUOW.WithAllThatMatch > pageRows
5369 && CurrentUOW.RetGrid.Count() >= CurrentUOW.WithAllThatMatch)
5373 if (!capReached && pages < CurrentUOW.PageCount - 1)
5378 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"No next page, breaking loop. Retrieved [{pages + 1}] out of [{CurrentUOW.PageCount}] pages.",
this, GPALObjectType.Browser);
5388 if (
null == returnGrid)
5389 returnGrid = (IGPALGrid<string>)CurrentUOW.RetGrid;
5391 returnGrid.Add((IGPALGrid<string>)CurrentUOW.RetGrid);
5400 private static string RowDedupeKey(List<string> row)
5402 return string.Join(
"\u001F", row);
5415 private bool TryGetGridFromTable(ref IGPALGrid<string> returnGrid)
5420 || SelectorType.Selector != CurrentUOW.
WithSelectorList[0].selectorSettings.SelectorType)
5427 bool matchedAll; List<GPALElement> matchedElems;
5428 ReadOnlyCollection<GPALElement> seeds = ElementHelper.FindWebElements(
this, CurrentUOW, sel, out matchedAll, out matchedElems);
5429 string tag = (
null != seeds && seeds.Count > 0) ? seeds[0].TagName?.ToLowerInvariant() :
null;
5430 bool isTable =
"table" == tag;
5431 bool isRows =
"tr" == tag;
5432 if (!isTable && !isRows)
5435 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"GetGrid: [{sel.Name}] resolved to <{tag}>; extracting the table structurally.",
this, GPALObjectType.Browser);
5437 var seenRows =
new HashSet<string>();
5438 List<string> headers =
null;
5439 bool capReached =
false;
5443 for (
int page = 0; page < CurrentUOW.PageCount && !capReached; page++)
5447 seeds = ElementHelper.FindWebElements(
this, CurrentUOW, sel, out matchedAll, out matchedElems);
5448 if (
null == seeds || 0 == seeds.Count)
5451 BrowserHelper.CheckDocumentReady(
this);
5454 var bodyRows =
new List<GPALElement>();
5459 var headerRows = FindWithin(table,
"thead tr");
5460 var tableBody = FindWithin(table,
"tbody tr");
5461 if (0 == tableBody.Count && 0 == headerRows.Count)
5464 var all = FindWithin(table,
"tr");
5465 if (all.Count > 0 && RowIsAllHeader(all[0]))
5467 headerRows.Add(all[0]);
5468 tableBody.AddRange(all.Skip(1));
5471 tableBody.AddRange(all);
5473 else if (0 == headerRows.Count && 0 < tableBody.Count &&
true == RowIsAllHeader(tableBody[0]))
5478 headerRows.Add(tableBody[0]);
5479 tableBody.RemoveAt(0);
5481 if (
null == headers && headerRows.Count > 0)
5483 var hmatrix = BuildTableMatrix(headerRows);
5484 if (hmatrix.Count > 0)
5485 headers = hmatrix[0];
5487 bodyRows.AddRange(tableBody);
5492 bodyRows.AddRange(seeds);
5496 List<List<string>> matrix = BuildTableMatrix(bodyRows);
5498 foreach (List<string> row
in matrix)
5500 string key =
string.Join(
"␁", row);
5501 if (!seenRows.Add(key))
5503 CurrentUOW.RetGrid.AddRow(row);
5504 if (CurrentUOW.RetGrid.Count() >= cap)
5511 if (!capReached && page < CurrentUOW.PageCount - 1)
5513 BrowserHelper.TopBrowser(
this,
false);
5514 if (!BrowserHelper.GotoNextPage(
this))
5516 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"GetGrid: no next page after page [{page + 1}].",
this, GPALObjectType.Browser);
5525 if (
null != headers && headers.Count > 0)
5527 CurrentUOW.ControlHeaderList = headers;
5529 if (
null == CurrentUOW.HeaderList || 0 == CurrentUOW.HeaderList.Count)
5530 CurrentUOW.HeaderList = headers;
5534 ColumnNames =
null == headers ?
new List<string>() : new List<string>(headers);
5536 returnGrid = (IGPALGrid<string>)CurrentUOW.RetGrid;
5542 private List<GPALElement> FindWithin(GPALElement parent,
string css)
5544 Selector scoped = GPAL.Selector.WithCSS(css).ToGPALObject();
5545 bool matchedAll; List<GPALElement> matchedElems;
5546 ReadOnlyCollection<GPALElement> found = ElementHelper.FindWebElements(
this, CurrentUOW, scoped, out matchedAll, out matchedElems, webElement: parent);
5547 return (
null != found) ? found.ToList() :
new List<GPALElement>();
5551 private bool RowIsAllHeader(GPALElement tr)
5553 var cells = FindWithin(tr,
"td, th");
5554 if (0 == cells.Count)
5556 foreach (GPALElement c
in cells)
5557 if (!
"th".Equals(c.
TagName, StringComparison.OrdinalIgnoreCase))
5565 private List<List<string>> BuildTableMatrix(List<GPALElement> rowElems)
5567 var matrix =
new List<List<string>>();
5569 var pending =
new Dictionary<int, KeyValuePair<int, string>>();
5571 foreach (GPALElement tr
in rowElems)
5573 var cells = FindWithin(tr,
"td, th");
5574 var row =
new List<string>();
5575 int col = 0, ci = 0;
5577 while (ci < cells.Count || pending.Keys.Any(k => k >= col))
5579 KeyValuePair<int, string> carry;
5580 if (pending.TryGetValue(col, out carry))
5582 row.Add(carry.Value);
5583 int left = carry.Key - 1;
5584 if (left > 0) pending[col] =
new KeyValuePair<int, string>(left, carry.Value);
5585 else pending.Remove(col);
5590 if (ci < cells.Count)
5592 GPALElement cell = cells[ci++];
5593 string text = CellText(cell);
5594 int cspan = ParseSpan(cell,
"colspan");
5595 int rspan = ParseSpan(cell,
"rowspan");
5596 for (
int c = 0; c < cspan; c++)
5599 if (rspan > 1) pending[col] =
new KeyValuePair<int, string>(rspan - 1, text);
5606 row.Add(GPAL.ErrorPlaceholder);
5616 foreach (var r
in matrix)
if (r.Count > width) width = r.Count;
5617 foreach (var r
in matrix)
while (r.Count < width) r.Add(GPAL.ErrorPlaceholder);
5622 private static int ParseSpan(GPALElement cell,
string attr)
5626 return (
int.TryParse(v, out n) && n > 0) ? n : 1;
5630 private string CellText(GPALElement cell)
5634 switch (cell.
TagName?.ToLowerInvariant())
5638 default:
return cell.Text ??
string.Empty;
5694 BrowserSettings.CallFilter = urlFragment;
5722 if (
false == CanCaptureCalls)
5724 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"No template: capturing calls is not available on [{BrowserSettings.AutomationEngine}]",
this, GPALObjectType.Browser);
5732 Stopwatch waited = Stopwatch.StartNew();
5734 while (
null == found && CallTemplateWaitMilliseconds > waited.ElapsedMilliseconds)
5736 CaptureCalls(out List<GPALCall> calls);
5745 System.Threading.Thread.Sleep(250);
5752 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"No call matching [{BrowserSettings.CallFilter}] in [{waited.ElapsedMilliseconds}]ms, so there is no template",
this, GPALObjectType.Browser);
5758 .WithPath(found.
Url)
5759 .WithHttpMethod(
"POST".Equals(found.
Method, StringComparison.OrdinalIgnoreCase) ? Enums.HttpVerb.Post : Enums.HttpVerb.Get)
5765 foreach (KeyValuePair<string, string> header
in found.Headers)
5767 template.WithHeader($
"{header.Key}: {header.Value}");
5769 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Template taken from the page's own [{found.Method}] to [{found.Url}] after [{waited.ElapsedMilliseconds}]ms, with [{template.Headers.Count}] of its headers",
this, GPALObjectType.Browser);
5775 private const int CallTemplateWaitMilliseconds = 30_000;
5779 private bool CanCaptureCalls =>
true == BrowserSettings.UsePuppeteer
5780 ||
true == BrowserSettings.UseOttoMagic
5790 if (
false == CanCaptureCalls)
5794 BrowserSettings.CaptureUnavailableReported =
true;
5796 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Capturing calls is not available on [{BrowserSettings.AutomationEngine}]. It needs a browser that reports its requests, and this one has none started",
this, GPALObjectType.Browser);
5806 BrowserSettings.CaptureCalls = capture;
5815 BrowserSettings.CaptureCalls = capture;
5817 if (
true == capture)
5820 BrowserHelper.SeleniumAddScriptToEvaluateOnNewDocument(
BrowserSettings, BrowserHelper.CallRecorderScript);
5822 BrowserHelper.ExecuteJavaScript(
this, BrowserHelper.CallRecorderScript);
5829 BrowserSettings.CaptureCalls = capture;
5834 BrowserSettings.PuppeteerCommunicator.CaptureCalls(capture).GetAwaiter().GetResult();
5859 calls =
true == BrowserSettings.UsePuppeteer
5860 ?
true == BrowserSettings.HasPuppeteerCommunicator
5861 ?
new List<GPALCall>(
BrowserSettings.PuppeteerCommunicator.CapturedCalls)
5862 :
new List<GPALCall>()
5863 :
true == BrowserSettings.UseOttoMagic
5864 ? ExtensionRecordedCalls()
5869 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"Handing back [{calls.Count}] captured calls.",
this, GPALObjectType.Browser);
5898 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG,
"Forgot the calls captured so far.",
this, GPALObjectType.Browser);
5906 private static string CdpResourceType(
string webRequestType)
5908 string retVal = webRequestType;
5910 switch (webRequestType)
5913 case "sub_frame": retVal =
"Document";
break;
5914 case "xmlhttprequest": retVal =
"XHR";
break;
5915 case "script": retVal =
"Script";
break;
5917 case "imageset": retVal =
"Image";
break;
5918 case "stylesheet": retVal =
"Stylesheet";
break;
5919 case "font": retVal =
"Font";
break;
5920 case "media": retVal =
"Media";
break;
5921 case "websocket": retVal =
"WebSocket";
break;
5922 case "ping": retVal =
"Ping";
break;
5923 case "csp_report": retVal =
"CSPViolationReport";
break;
5928 case "other": retVal =
"Other";
break;
5936 private List<GPALCall> ExtensionRecordedCalls()
5938 List<GPALCall> retVal =
new List<GPALCall>();
5942 if (
true ==
string.IsNullOrWhiteSpace(json))
return retVal;
5947 Newtonsoft.Json.Linq.JObject snapshot;
5953 snapshot = Newtonsoft.Json.Linq.JObject.Parse(json);
5955 catch (GPALException)
5959 catch (Exception ex)
5961 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"The captured calls could not be read. [{json.Length}] characters starting [{json.Substring(0, Math.Min(120, json.Length))}]",
this, GPALObjectType.Browser, ex);
5968 int dropped = (
int?)snapshot[
"dropped"] ?? 0;
5971 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"The recorder dropped [{dropped}] older calls to stay within its limit",
this, GPALObjectType.Browser);
5973 foreach (Newtonsoft.Json.Linq.JObject recorded in snapshot[
"calls"] ??
new Newtonsoft.Json.Linq.JArray())
5975 GPALCall call =
new GPALCall
5977 Url = recorded[
"url"]?.ToString(),
5978 Method = recorded[
"method"]?.ToString(),
5979 ResourceType = CdpResourceType(recorded[
"type"]?.ToString()),
5980 PostData = recorded[
"postData"]?.ToString(),
5981 Initiator = recorded[
"initiator"]?.ToString(),
5982 Status = (
int?)recorded[
"status"] ?? 0
5985 if (recorded[
"headers"] is Newtonsoft.Json.Linq.JObject headers)
5986 foreach (KeyValuePair<string, Newtonsoft.Json.Linq.JToken> header
in headers)
5987 call.
Headers[header.Key] = header.Value?.ToString();
5997 private List<GPALCall> RecordedCalls()
5999 List<GPALCall> retVal =
new List<GPALCall>();
6003 string json = BrowserHelper.ExecuteJavaScript(
this,
"return JSON.stringify(window.__gpalCalls || []);");
6005 if (
true ==
string.IsNullOrWhiteSpace(json))
return retVal;
6007 foreach (Newtonsoft.Json.Linq.JObject recorded in Newtonsoft.Json.Linq.JArray.Parse(json))
6009 GPALCall call =
new GPALCall
6011 Url = recorded[
"url"]?.ToString(),
6012 Method = recorded[
"method"]?.ToString(),
6013 ResourceType = recorded[
"type"]?.ToString(),
6014 PostData = recorded[
"postData"]?.ToString(),
6015 Initiator =
"script",
6016 Status = (
int?)recorded[
"status"] ?? 0
6019 if (recorded[
"headers"] is Newtonsoft.Json.Linq.JObject headers)
6020 foreach (KeyValuePair<string, Newtonsoft.Json.Linq.JToken> header
in headers)
6021 call.
Headers[header.Key] = header.Value?.ToString();
6029 public IAllowWithHeaderOrFileActions Fetch(GPALRequest request)
6031 CurrentUOW.FetchResults.Clear();
6033 if (
null == request)
6034 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"No request given to fetch.",
this, GPALObjectType.Browser);
6036 BrowserHelper.FetchWithTokens(
this, request);
6049 List<string> values =
new List<string>();
6051 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Handing back [{CurrentUOW.FetchResults.Count}] fetch responses.",
this, GPALObjectType.Browser);
6056 foreach (
string body
in CurrentUOW.FetchResults)
6057 values.Add(
true == StringFormatDetector.IsJson(body.Trim()) ? body : Newtonsoft.Json.JsonConvert.SerializeObject(body));
6059 data =
"[" +
string.Join(
",", values) +
"]";
6066 var converter =
GPAL.
Converter.WithInput(CurrentUOW.RetGrid).WithColumnNames(ResolveHeaderColumns(file));
6070 converter.WithFirstLineHasColumnNames(
true);
6071 converter.SaveTo(file);
6083 GPAL.
Converter.WithInput(CurrentUOW.RetGrid).WithColumnNames(ResolveHeaderColumns(file)).AppendTo(file);
6100 private List<string> ResolveHeaderColumns(
GPALFile file)
6102 List<string> selectorNames = CurrentUOW.
WithSelectorList?.Where(s =>
null != s.Name).Select(s => s.Name).ToList()
6103 ??
new List<string>();
6105 List<string> fileColumns =
new List<string>();
6106 IGPALGrid<string> columnList = ((IGPALFileInternal)file).FileSettings.ColumnList;
6107 if (
null != columnList && 0 < columnList.Count() &&
null != columnList[0])
6108 fileColumns =
new List<string>(columnList[0]);
6110 List<string> resolved = (
null != CurrentUOW.HeaderList && 0 < CurrentUOW.
HeaderList.Count)
6112 :
new List<string>();
6113 for (
int i = resolved.Count; i < fileColumns.Count; i++)
6114 resolved.Add(fileColumns[i]);
6115 for (
int i = resolved.Count; i < selectorNames.Count; i++)
6116 resolved.Add(selectorNames[i]);
6120 List<string> controlHeaders = CurrentUOW.ControlHeaderList ??
new List<string>();
6122 for (
int i = resolved.Count; i < controlHeaders.Count; i++)
6123 resolved.Add(controlHeaders[i]);
6130 if (
null != CurrentUOW.RetGrid)
6131 foreach (List<string> row
in CurrentUOW.RetGrid)
6132 if (
null != row && widest < row.Count)
6135 for (
int i = resolved.Count; i < widest; i++)
6136 resolved.Add($
"column{i + 1}");
6138 return UniqueColumns(resolved);
6153 private static List<string> UniqueColumns(List<string> columns)
6155 var used =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
6156 var result =
new List<string>();
6158 for (
int i = 0; i < columns.Count; i++)
6160 string name =
true ==
string.IsNullOrWhiteSpace(columns[i]) ? $
"column{i + 1}" : columns[i].Trim();
6161 string candidate = name;
6164 while (
false == used.Add(candidate))
6167 candidate = $
"{name}{repeat}";
6170 result.Add(candidate);
6188 if (
true == CurrentUOW.ActionCalled)
6190 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Sleeping for [{waitForTimeInMs}] ms",
this, GPALObjectType.Browser);
6191 Thread.Sleep(waitForTimeInMs);
6194 CurrentUOW.WaitForInMs = waitForTimeInMs;
6205 bool weSetWaitFor =
false;
6206 WaitTime safe = WaitTime.Never;
6209 if (WaitTime.Never == CurrentUOW.WaitForInMs)
6211 safe = CurrentUOW.WaitForInMs;
6212 CurrentUOW.WaitForInMs = WaitTime.Immediate;
6213 weSetWaitFor =
true;
6217 WorkflowSetup(
true,
true);
6219 if (
true == weSetWaitFor)
6220 CurrentUOW.WaitForInMs = safe;
6232 if (ElementState.NotSet != elementState)
6234 CurrentUOW.WaitForElementState = elementState;
6236 if (
true == CurrentUOW.ActionCalled)
6237 ElementHelper.WaitFor(
this, CurrentUOW.WaitForInMs, out
bool matchedAll);
6240 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"ElementState NOTSET is not a valid wait condition. Please select a valid ElementState.",
null, GPALObjectType.None);
6252 FormHelper.WaitForWindow(waitForTitle,
BrowserSettings.WaitForWindowTimeoutInSeconds);
6263 FormHelper.WaitForWindowRegex(waitForTitleRegex,
BrowserSettings.WaitForWindowTimeoutInSeconds);
6273 BrowserSettings.PageOrientation = pageOrientation;
6290 List<string> selectorPaths =
new List<string>();
6293 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
6295 foreach (
Selector selector
in CurrentUOW.WithSelectorList)
6296 if (SelectorType.Selector == selector.SelectorType
6298 &&
false ==
string.IsNullOrEmpty(selector.
SelectorPaths[0].SelectorPath))
6306 string css = SelectorPathType.Image == selector.
SelectorPaths[0].SelectorPathType
6307 ? ElementHelper.CssUnderImage(
this, selector)
6308 : SelectorPathType.Xpath == selector.
SelectorPaths[0].SelectorPathType
6309 ? ElementHelper.CssForXPath(
this, selector)
6313 selectorPaths.Add(css);
6319 selectorPaths.Add(
"html");
6321 foreach (
string selectorPath
in selectorPaths)
6322 PrintOneToPDF(selectorPath, gpalFile.
Next);
6335 private void PrintOneToPDF(
string selectorPath,
GPALFile outputFile)
6337 string fullHtml =
null;
6348 $@"var div = document.querySelector('{selectorPath}'); return div.outerHTML;");
6352 function getAllCss() {{
6354 var stylesheets = document.styleSheets;
6355 for (var i = 0; i < stylesheets.length; i++) {{
6357 var rules = stylesheets[i].cssRules;
6358 for (var j = 0; j < rules.length; j++) {{
6359 css += rules[j].cssText + ' ';
6361 }} catch (e) {{ /* Ignore cross-origin stylesheet errors */ }}
6366 function getElementCss(element) {{
6368 var style = window.getComputedStyle(element);
6369 for (var i = 0; i < style.length; i++) {{
6370 css += style[i] + ': ' + style.getPropertyValue(style[i]) + '; ';
6375 var allCss = getAllCss();
6376 var div = document.querySelector('{selectorPath}');
6377 var elementCss = getElementCss(div);
6379 return '<style>' + allCss + ' ' + elementCss + '</style>';
6381 fullHtml =
$@"<html><head>{cssContent}</head><body>{elementContent}</body></html>";
6391 string linksScript =
@"(function () {
6392 var head = '<base href=""' + location.href.replace(/""/g, '"') + '"">';
6393 var links = document.querySelectorAll('link[rel~=""stylesheet""][href]');
6394 for (var i = 0; i < links.length; i++)
6395 head += '<link rel=""stylesheet"" href=""' + links[i].href.replace(/""/g, '"') + '"">';
6398 string pageLinks = BrowserHelper.ExecuteJavaScript(
this,
true ==
UseSelenium ?
"return " + linksScript : linksScript);
6400 if (
true == pageLinks?.StartsWith(
"<base"))
6402 int head = fullHtml?.IndexOf(
"<head>", StringComparison.OrdinalIgnoreCase) ?? -1;
6404 fullHtml = 0 <= head
6405 ? fullHtml.Insert(head +
"<head>".Length, pageLinks)
6406 : pageLinks + fullHtml;
6410 #region NEW get the browser to print to pdf without CDP
6414 string pageCss =
"<style>@page { size: "
6415 + (PageOrientation.Landscape == BrowserSettings.PageOrientation ?
"landscape" :
"portrait")
6416 +
"; margin: 0; } html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }</style>";
6417 string outputHtmlPath = $
"{outputFile.Filename}.html";
6420 File.WriteAllText(outputHtmlPath, pageCss + fullHtml);
6425 string printerPath =
null;
6427 printerPath = Puppeteer.GetChromePath();
6429 printerPath = Puppeteer.GetEdgePath();
6431 if (
string.IsNullOrEmpty(printerPath) ||
false == File.Exists(printerPath))
6433 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
6434 $
"PrintToPDF needs Chrome or Edge, [{BrowserSettings.BrowserType}] cannot print [{outputFile.Filename}]",
6439 string printerProfile = ChromeProfileManager.NewTempProfilePath();
6440 string fileUrl = $
"file:///{Path.GetFullPath(outputHtmlPath).Replace("\\
", "/
")}";
6441 string printerArgs =
"--headless --disable-gpu --no-first-run --no-default-browser-check"
6442 + $
" --user-data-dir=\"{printerProfile}\""
6443 +
" --no-pdf-header-footer"
6444 + $
" --print-to-pdf=\"{Path.GetFullPath(outputFile.Filename)}\""
6445 + $
" \"{fileUrl}\"";
6447 System.Diagnostics.Process pdfPrinter = System.Diagnostics.Process.Start(
new System.Diagnostics.ProcessStartInfo(printerPath, printerArgs)
6449 UseShellExecute = false,
6450 CreateNoWindow = true
6454 bool exitedCleanly = pdfPrinter.WaitForExit(30000);
6460 if (!pdfPrinter.HasExited)
6465 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
6466 "PrintToPDF process did not exit cleanly (timeout)",
6469 else if (pdfPrinter.ExitCode != 0)
6471 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
6472 $
"PrintToPDF exited with code [{pdfPrinter.ExitCode}]",
6476 ChromeProfileManager.RemoveTempUserProfile(printerProfile);
6482 File.Delete(outputHtmlPath);
6484 catch (GPALException)
6488 catch (Exception ex)
6491 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Failed to delete temporary files",
BrowserSettings.Browser, GPALObjectType.Browser, ex);
6493 #endregion NEW get the browser to print to pdf without CDP
6495 #region OLD use CDP to page.PrintToPDF a window
6496 BrowserHelper.ExecuteJavaScript(
@"
6497 var newWindow = window.open('', '_blank');
6498 newWindow.document.open();
6499 newWindow.document.write('<html><head>' + arguments[0] + '</head><body>' + arguments[1] + '</body></html>');
6500 newWindow.document.close();
6502 ", browserSettings.Browser,
new { cssContent, elementContent });
6505 System.Threading.Thread.Sleep(2000);
6508 var windows = browserSettings.BrowserDriver.WindowHandles;
6509 browserSettings.BrowserDriver.SwitchTo().Window(windows[windows.Count - 1]);
6512 var printSettings =
new Dictionary<string, object>
6514 {
"printBackground",
true },
6515 {
"landscape",
false }
6519 dynamic result =
null;
6521 if (
BrowserType.Chrome == browserSettings.BrowserType)
6522 result = ((OpenQA.Selenium.Chromium.ChromiumDriver)browserSettings.BrowserDriver).ExecuteCdpCommand(
"Page.printToPDF", printSettings);
6523 else if (
BrowserType.Edge == browserSettings.BrowserType)
6524 result = ((OpenQA.Selenium.Edge.EdgeDriver)browserSettings.BrowserDriver).ExecuteCdpCommand(
"Page.printToPDF", printSettings);
6526 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{browserSettings.BrowserType}] does not support Page.printToPDF", browserSettings.Browser, GPALObjectType.Browser);
6528 var resultDict = result as IDictionary<string, object>;
6530 if (resultDict !=
null && resultDict.ContainsKey(
"data"))
6533 var pdfData = Convert.FromBase64String(resultDict[
"data"].ToString());
6535 System.IO.File.WriteAllBytes(outputFile.
Filename, pdfData);
6538 browserSettings.BrowserDriver.Close();
6539 windows = browserSettings.BrowserDriver.WindowHandles;
6540 browserSettings.BrowserDriver.SwitchTo().Window(windows[windows.Count - 1]);
6541 #endregion OLD use CDP to page.PrintToPDF a window
6553 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
6555 ElementHelper.SendString(
this, textToSend);
6579 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Pressed [{GetModifierKeys(modifierKeys)}]",
this, GPALObjectType.Browser);
6581 if (ModifierKeys.NONE != modifierKeys)
6584 HardwareHelper.PressModifierKey(modifierKeys);
6597 bool anyKeySet =
false;
6599 if (modifierKeys.HasFlag(ModifierKeys.Alt))
6601 actions.KeyDown(Keys.Alt);
6604 if (modifierKeys.HasFlag(ModifierKeys.Control))
6606 actions.KeyDown(Keys.Control);
6609 if (modifierKeys.HasFlag(ModifierKeys.Shift))
6611 actions.KeyDown(Keys.Shift);
6614 if (modifierKeys.HasFlag(ModifierKeys.Windows))
6616 actions.KeyDown(Keys.Meta);
6620 ModifierKeys unsupported = modifierKeys & (ModifierKeys.Application | ModifierKeys.ScrollLock);
6621 if (ModifierKeys.NONE != unsupported)
6622 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"PressModifierKey: [{GetModifierKeys(unsupported)}] is not supported via Selenium/WebDriver Actions and was ignored.",
this, GPALObjectType.Browser);
6624 if (
true == anyKeySet)
6647 public IAllowBrowserActionOrAnySelector
ReleaseModifierKey(ModifierKeys modifierKeys)
6649 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Released [{GetModifierKeys(modifierKeys)}]",
this, GPALObjectType.Browser);
6651 if (ModifierKeys.NONE != modifierKeys)
6654 HardwareHelper.ReleaseModifierKeys(modifierKeys);
6667 bool anyKeySet =
false;
6669 if (modifierKeys.HasFlag(ModifierKeys.Alt))
6671 actions.KeyUp(Keys.Alt);
6674 if (modifierKeys.HasFlag(ModifierKeys.Control))
6676 actions.KeyUp(Keys.Control);
6679 if (modifierKeys.HasFlag(ModifierKeys.Shift))
6681 actions.KeyUp(Keys.Shift);
6684 if (modifierKeys.HasFlag(ModifierKeys.Windows))
6686 actions.KeyUp(Keys.Meta);
6690 ModifierKeys unsupported = modifierKeys & (ModifierKeys.Application | ModifierKeys.ScrollLock);
6691 if (ModifierKeys.NONE != unsupported)
6692 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"ReleaseModifierKey: [{GetModifierKeys(unsupported)}] is not supported via Selenium/WebDriver Actions and was ignored.",
this, GPALObjectType.Browser);
6694 if (
true == anyKeySet)
6721 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
6723 ElementHelper.SendKey(
this, VKCode);
6740 InAction(
false,
false);
6743 MagicHelper.ScrollWindowHorizontal(scrollAmountInPixels);
6745 PuppeteerClient.ScrollWindowByHorizontal(scrollAmountInPixels).Execute();
6763 InAction(
false,
false);
6766 MagicHelper.ScrollWindowVertical(scrollAmountInPixels);
6768 PuppeteerClient.ScrollWindowByVertical(scrollAmountInPixels).Execute();
6787 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
6788 ElementHelper.ScrollElement(
this, scrollAmountInPixels, ScrollTypes.Horizontal);
6805 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
6806 ElementHelper.ScrollElement(
this, scrollAmountInPixels, ScrollTypes.Vertical);
6833 UpdateActiveTab(URLorTabTuple);
6835 UpdateActiveTab(gotoTabTuple);
6839 if (URLorTabTuple is GPALUrl url)
6841 else if (URLorTabTuple is
int tabId)
6848 string currentUrl = this.
BrowserDriver.SwitchTo().Window(handle).Url;
6849 if (currentUrl == URLorTabTuple)
6852 BrowserSettings.CurrentURL = URLorTabTuple;
6862 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to detect tab with URL [{URLorTabTuple}]",
this, GPALObjectType.Browser);
6874 public IAllowBrowserActionOrAnySelector
DragAndDrop(ModifierKeys modifierKeys)
6877 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
6879 ElementHelper.DragAndDrop(
this, modifierKeys);
6892 if (
null != selector)
6896 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
6898 ElementHelper.DragAndDrop(
this);
6925 private readonly List<string> _injectedScriptIdentifiers =
new List<string>();
6928 private bool _scriptsRegistered =
false;
6929 private readonly List<string> _pendingInjectedScripts =
new List<string>();
6932 private bool _dialogRegistered =
false;
6935 private string _dialogPushed =
null;
6938 private int _dialogSeq = 0;
6944 private bool EngineReachable
6951 retVal =
false ==
string.IsNullOrEmpty(
BrowserSettings.RestApiBaseUrl);
6966 private void RegisterInjectedScript(
string script)
6968 _scriptsRegistered =
true;
6980 var identifier = BrowserHelper.SeleniumAddScriptToEvaluateOnNewDocument(
BrowserSettings, script);
6981 if (
false ==
string.IsNullOrEmpty(identifier))
6982 _injectedScriptIdentifiers.Add(identifier);
6991 internal void ApplyPendingInjectedScripts()
6993 if (0 < _pendingInjectedScripts.Count &&
true == EngineReachable)
6995 foreach (var script
in _pendingInjectedScripts)
6996 RegisterInjectedScript(script);
6998 _pendingInjectedScripts.Clear();
7023 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Selenium takes this when the browser starts, and this one is already running, so dialogs are not answered. Call WithDialogsAccepted before the first GoTo",
this, GPALObjectType.Browser);
7025 BrowserSettings.DialogsAccepted = accept;
7027 ArmDialogHandling();
7039 BrowserSettings.DialogText = text;
7044 BrowserSettings.DialogsAccepted =
true;
7046 ArmDialogHandling();
7064 BrowserSettings.DialogsAccepted =
null;
7065 BrowserSettings.DialogText =
null;
7067 ArmDialogHandling();
7076 internal void ArmDialogHandling()
7080 if (
false == EngineReachable || (
null ==
BrowserSettings.DialogsAccepted &&
false == _dialogRegistered))
7083 if (
true == InPageDialogs)
7085 if (
false == _dialogRegistered &&
null !=
BrowserSettings.DialogsAccepted)
7087 string script = DialogScript();
7089 _dialogRegistered =
true;
7097 if (
true == _navigated)
7098 BrowserHelper.ExecuteJavaScript(
this, script);
7100 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Answering dialogs in the page, accept [{BrowserSettings.DialogsAccepted}]",
this, GPALObjectType.Browser);
7108 if (
true == _navigated && written != _dialogPushed)
7110 _dialogPushed = written;
7111 PushDialogAnswers();
7119 if (
true ==
UseOttoMagic &&
true == _navigated &&
false == _dialogChecked)
7121 _dialogChecked =
true;
7125 string seen = BrowserHelper.ExecuteJavaScript(
this,
"window.__gpalDialogSeq");
7127 if (
true ==
string.IsNullOrEmpty(seen) ||
"null" == seen)
7128 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"The dialog override is not on the page, so dialogs will stop this workflow. The usual cause is Allow user scripts being off for the extension",
this, GPALObjectType.Browser);
7135 _dialogRegistered =
true;
7139 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Answering dialogs over CDP, accept [{BrowserSettings.DialogsAccepted}]",
this, GPALObjectType.Browser);
7160 private bool _navigated =
false;
7163 private bool _dialogChecked =
false;
7170 private void PushDialogAnswers()
7173 BrowserHelper.ExecuteJavaScript(
this,
"if (window.__gpalDialogSeq) { " + DialogAnswers() +
" }");
7181 private string DialogAnswers()
7183 string accept =
null == BrowserSettings.DialogsAccepted
7185 :
true == BrowserSettings.DialogsAccepted ?
"true" :
"false";
7186 string typed =
null == BrowserSettings.DialogText ?
"null" : JsonConvert.SerializeObject(
BrowserSettings.DialogText);
7188 return "window.__gpalDialogAccept = " + accept +
"; window.__gpalDialogAnswer = " + typed +
";";
7197 private string DialogScript()
7201 " var seq = " + (++_dialogSeq) +
";" +
7202 " if (window.__gpalDialogSeq >= seq) return;" +
7203 " window.__gpalDialogSeq = seq;" +
7204 " " + DialogAnswers() +
7205 " var asText = Function.prototype.toString;" +
7206 " function stand(name, fn) {" +
7207 " Object.defineProperty(fn, 'name', { value: name, configurable: true });" +
7212 " var onto = Object.getOwnPropertyDescriptor(window, name) ? window : Window.prototype;" +
7213 " Object.defineProperty(onto, name," +
7214 " { value: fn, writable: true, enumerable: true, configurable: true });" +
7218 " var real = { alert: window.alert, confirm: window.confirm, prompt: window.prompt };" +
7219 " function off() { return null === window.__gpalDialogAccept; }" +
7220 " stand('alert', function (message) {" +
7221 " if (off()) return real.alert.call(window, message);" +
7223 " stand('confirm', function (message) {" +
7224 " if (off()) return real.confirm.call(window, message);" +
7225 " return true === window.__gpalDialogAccept;" +
7227 " stand('prompt', function (message, preset) {" +
7228 " if (off()) return real.prompt.call(window, message, preset);" +
7229 " if (true !== window.__gpalDialogAccept) return null;" +
7230 " return null !== window.__gpalDialogAnswer ? window.__gpalDialogAnswer : (preset || '');" +
7234 " Function.prototype.toString = function () {" +
7235 " if (this === window.alert || this === window.confirm || this === window.prompt" +
7236 " || this === Function.prototype.toString)" +
7237 " return 'function ' + this.name + '() { [native code] }';" +
7238 " return asText.call(this);" +
7258 window = Rectangle.Empty;
7260 if (
true == InAction(
false))
7265 ElementHelper.GetWindowRectangle(
this, out window);
7282 if (
false == EngineReachable)
7283 _pendingInjectedScripts.Add(script);
7285 RegisterInjectedScript(script);
7299 foreach (var filename
in scriptFile.
Filenames)
7312 _pendingInjectedScripts.Clear();
7314 if (
true == EngineReachable)
7326 foreach (var identifier
in _injectedScriptIdentifiers)
7328 _injectedScriptIdentifiers.Clear();
7340 public IAllowBrowserActionOrAnySelector
SetRange(
int rangeValue)
7342 if (
true == InAction(WaitTime.Never < CurrentUOW.WaitForInMs))
7345 ElementHelper.FindWebElements(
this, CurrentUOW, selector, out _, out List<GPALElement> elems);
7348 ElementHelper.ScrollIntoView(
this, elem);
7358 arguments[0].value = arguments[1];
7359 arguments[0].dispatchEvent(new Event('input', { bubbles: true }));
7360 arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
7370 #endregion <Browser Actions>
7372 private bool _started =
false;
7376 internal bool Started
7379 set => _started = value;
7387 set => BrowserSettings.ServerResponseCode = value;
7396 internal set => BrowserSettings.CurrentURL = value;
7432 internal set => BrowserSettings.AutomationEngine = value;
7446 BrowserSettings.FileDownloaded = value;
7463 internal UnitOfWork CurrentUOW {
get;
set; } =
null;
7467 internal WorkflowManager WorkflowManager {
get;
set; }
7471 internal string Name
7480 BrowserSettings.BrowserName = value;
7497 BrowserSettings.BrowserDriver = value;
7518 return _browserSettings;
7523 _browserSettings = value;
7529 internal string DownloadLocation
7542 internal set => BrowserSettings.PuppeteerCommunicator = value;
7558 try { System.Diagnostics.Process.GetProcessById(
BrowserSettings.ServiceDriverPid);
return true; }
7559 catch (ArgumentException) {
return false; }
7570 internal set => BrowserSettings.PuppeteerClient = value;
7591 $
"No OttoMagic REST API on [{BrowserSettings.AutomationEngine}]. Only the OttoMagic engines run one. Use PuppeteerClient on a Puppeteer engine, or the browser's own actions",
7592 this, GPALObjectType.Browser);
7603 public IHiddenDesktop
HiddenDesktop =>
true == BrowserSettings.HiddenDesktop
7606 #endregion <Getters>
7615 bool ScrollPage(
string direction)
7620 if (direction !=
"up" && direction !=
"down")
7627 (function(direction) {
7628 function scrollPage(direction) {
7630 const isDown = direction === 'down';
7631 const scrollAmount = isDown ? window.innerHeight : -window.innerHeight;
7632 const keyName = isDown ? 'PageDown' : 'PageUp';
7633 const keyCode = isDown ? 34 : 33;
7636 const overflow = getComputedStyle(document.documentElement).overflow;
7638 // Try window.scrollBy if overflow is not 'hidden'
7639 const initialScrollY = window.scrollY;
7640 if (overflow !== 'hidden') {
7641 window.scrollBy({ left: 0, top: scrollAmount, behavior: 'instant' });
7642 if (window.scrollY !== initialScrollY) {
7645 method: 'window.scrollBy',
7646 scrollPosition: window.scrollY,
7647 elementClass: 'window'
7652 // Try scrolling the scrollable element
7653 const scrollable = Array.from(document.querySelectorAll('*')).find(
7654 el => el.scrollHeight > el.clientHeight && getComputedStyle(el).overflowY !== 'hidden'
7656 const initialScrollTop = scrollable.scrollTop;
7657 scrollable.scrollBy({ left: 0, top: scrollAmount, behavior: 'instant' });
7658 const elementClass = scrollable.className || 'body';
7660 if (scrollable.scrollTop !== initialScrollTop) {
7663 method: 'element.scrollBy',
7664 scrollPosition: scrollable.scrollTop,
7669 // Fallback to Page Up/Down key press
7671 const event = new KeyboardEvent('keydown', {
7679 scrollable.dispatchEvent(event);
7681 if (scrollable.scrollTop !== initialScrollTop) {
7685 scrollPosition: scrollable.scrollTop,
7690 // No scroll method worked
7693 message: 'No scroll method worked',
7694 scrollPosition: scrollable.scrollTop,
7700 message: error.message,
7702 elementClass: 'none'
7706 return scrollPage(direction);
7707 })('" + direction +
"')";
7710 var result = BrowserHelper.ExecuteJavaScript(
this, script);
7713 dynamic response = result;
7714 return response.status ==
"success";
7727 private bool ScrollToPosition(
string position)
7732 if (position !=
"top" && position !=
"end")
7739 (function(position) {
7740 function scrollToPosition(position) {
7742 const isEnd = position === 'end';
7743 const scrollTarget = isEnd ? document.body.scrollHeight : 0;
7744 const keyName = isEnd ? 'End' : 'Home';
7745 const keyCode = isEnd ? 35 : 36;
7748 const overflow = getComputedStyle(document.documentElement).overflow;
7750 // Try window.scrollTo if overflow is not 'hidden'
7751 const initialScrollY = window.scrollY;
7752 if (overflow !== 'hidden') {
7753 window.scrollTo({ left: 0, top: scrollTarget, behavior: 'instant' });
7754 if (window.scrollY !== initialScrollY) {
7757 method: 'window.scrollTo',
7758 scrollPosition: window.scrollY,
7759 elementClass: 'window'
7764 // Try scrolling the scrollable element
7765 const scrollable = Array.from(document.querySelectorAll('*')).find(
7766 el => el.scrollHeight > el.clientHeight && getComputedStyle(el).overflowY !== 'hidden'
7768 const initialScrollTop = scrollable.scrollTop;
7769 const elementScrollTarget = isEnd ? scrollable.scrollHeight : 0;
7770 scrollable.scrollTo({ left: 0, top: elementScrollTarget, behavior: 'instant' });
7771 const elementClass = scrollable.className || 'body';
7773 if (scrollable.scrollTop !== initialScrollTop) {
7776 method: 'element.scrollTo',
7777 scrollPosition: scrollable.scrollTop,
7782 // Fallback to Home/End key press
7784 const event = new KeyboardEvent('keydown', {
7792 scrollable.dispatchEvent(event);
7794 if (scrollable.scrollTop !== initialScrollTop) {
7798 scrollPosition: scrollable.scrollTop,
7803 // No scroll method worked
7806 message: 'No scroll method worked',
7807 scrollPosition: scrollable.scrollTop,
7813 message: error.message,
7815 elementClass: 'none'
7819 return scrollToPosition(position);
7820 })('" + position +
"')";
7823 var result = BrowserHelper.ExecuteJavaScript(
this, script);
7826 dynamic response = result;
7827 return response.status ==
"success";
7839 private static void SelectorListMaintenanceSweep(UnitOfWork CurrentUOW)
7841 for (
int idx = 0; idx < CurrentUOW.WithSelectorList.Count; idx++)
7842 if (
true == CurrentUOW.WithSelectorList[idx].DeleteMe)
7843 CurrentUOW.WithSelectorList.RemoveAt(idx);
7845 for (
int idx = 0; idx < CurrentUOW.InSelectorList.Count; idx++)
7846 if (
true == CurrentUOW.InSelectorList[idx].DeleteMe)
7847 CurrentUOW.InSelectorList.RemoveAt(idx);
7853 private void CheckPersistentSelectors()
7855 UnitOfWork safeUOW = CurrentUOW;
7858 if (0 < (persistentUOW.WithSelectorList?.Count ?? 0))
7860 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Checking [{persistentUOW.WithSelectorList?.Count}] persistent selector(s)");
7862 foreach (Selector selector
in persistentUOW.WithSelectorList)
7864 if (SelectorType.Selector != selector.SelectorType)
7868 selector.WebSelectorFoundResults =
null;
7869 selector.WebSelectorMatchedResults =
null;
7870 CurrentUOW.CurrentSelector = selector;
7873 ElementHelper.FindWebElements(
this, persistentUOW, selector, out _, out _,
null,
true,
true);
7876 CurrentUOW = safeUOW;
7882 private bool CheckWaitFor()
7884 bool matchFound =
true;
7885 List<GPALElement> foundElements =
null;
7890 if (
null != CurrentUOW.WaitForSatisfied)
7892 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Selectors for this unit of work already found at [{CurrentUOW.WaitForSatisfied:HH:mm:ss.fff}], skipping redundant wait.",
this, GPALObjectType.Browser);
7898 if (WaitTime.Never < CurrentUOW.WaitForInMs)
7901 foundElements = ElementHelper.WaitFor(
this, CurrentUOW.WaitForInMs, out matchFound);
7903 if (0 < foundElements?.Count)
7904 CurrentUOW.WaitForSatisfied = DateTime.Now;
7907 return (0 < foundElements?.Count);
7917 internal bool WorkflowSetup(
bool checkWaitFor,
bool doMaintenance, GPALUrl URL =
null)
7923 BrowserSettings.HiddenDesktopReported =
true;
7932 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{BrowserSettings.AutomationEngine}] cannot drive a hidden desktop, so this browser runs on [{steppedDownTo}]",
this, GPALObjectType.Browser);
7934 BrowserSettings.AutomationEngine = steppedDownTo;
7941 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No hidden desktop could be made, so this browser opens on the desktop you are working on",
this, GPALObjectType.Browser);
7943 BrowserSettings.HiddenDesktop =
false;
7976 BrowserSettings.Process = LaunchWhereAsked(() =>
MagicHelper.LaunchBrowser(
this, URL));
7978 BrowserSettings.Process = LaunchWhereAsked(() => Puppeteer.LaunchBrowser(
this, URL));
7986 BrowserSettings.HiddenDesktopChecked =
true;
7988 if (
false == Desktops.HasWindows(
BrowserSettings.HiddenDesktopName, 10_000))
7989 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Nothing arrived on [{BrowserSettings.HiddenDesktopName}], so this browser is on the desktop you are working on",
this, GPALObjectType.Browser);
7995 BrowserHelper.TopBrowser(
this,
false);
7997 CurrentUOW.ActionCalled =
true;
7999 if (
true == doMaintenance)
8001 SelectorListMaintenanceSweep(CurrentUOW);
8002 SelectorListMaintenanceSweep(persistentUOW);
8003 CheckPersistentSelectors();
8006 if (
true == checkWaitFor)
8011 BrowserSettings.PuppeteerUrl = $
"http://127.0.0.1:{BrowserSettings.DebugPort}";
8020 BrowserSettings.DownloadBehaviorSet =
true;
8026 ApplyPendingInjectedScripts();
8027 ArmDialogHandling();
8031 bool firstRun {
get;
set; } =
true;
8032 bool inAction =
false;
8045 internal bool InAction(
bool checkWaitFor,
bool doMaintenance =
true, GPALUrl URL =
null)
8048 if (
false == inAction)
8055 retval = WorkflowSetup(checkWaitFor, doMaintenance, URL);
8078 private void BackOutOfSelfClosedTab(GPALUrl URL,
string priorUrl, GPALUrl priorGPALUrl, TabTuple priorTab,
string priorHandle)
8093 string landOn =
true == handles.Contains(priorHandle) ? priorHandle : handles[handles.Count - 1];
8099 BrowserSettings.CurrentURL = priorUrl;
8100 BrowserSettings.CurrentGPALUrl = priorGPALUrl;
8102 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"[{URL?.Url}] downloaded rather than opening a page, so its tab closed itself. Back on [{priorUrl}].",
this, GPALObjectType.Browser);
8113 internal int RealTabCount()
8128 private int GetActiveTabId()
8139 private void UpdateActiveTab(TabTuple activeTabTuple)
8141 string priorUrl =
null;
8142 if (
null != activeTabTuple)
8153 priorUrl = activeTabTuple.Url;
8154 BrowserSettings.TabIdsToURL.Add(activeTabTuple.TabId,
new TabInfo { URL = activeTabTuple.Url, ActiveTab = true });
8158 foreach (var key
in BrowserSettings.TabIdsToURL.Keys.Where(k => k != activeTabTuple.TabId))
8161 if (
null != activeTabTuple.Url)
8163 URLInfo uRLInfo =
new URLInfo { TabId = activeTabTuple.TabId, ActiveTab =
true };
8169 if (
false == priorUrl.Equals(activeTabTuple.Url))
8173 BrowserSettings.URLsToTabId.Add(activeTabTuple.Url,
new URLInfo { TabId = activeTabTuple.TabId, ActiveTab = true });
8176 foreach (var key
in BrowserSettings.URLsToTabId.Keys.Where(k => k != activeTabTuple.Url))
8179 BrowserSettings.CurrentURL = activeTabTuple.Url;
8184 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"ActiveTabTuple from last operation is null",
this, GPALObjectType.Browser);
8195 internal bool TryDecodeNumberToIpAddress(
string number, out
string ipAddress)
8197 bool retVal =
false;
8202 if (
true == UInt32.TryParse(number, out numberUInt))
8206 byte[] bytes = BitConverter.GetBytes(numberUInt);
8207 if (BitConverter.IsLittleEndian)
8208 Array.Reverse(bytes);
8213 var ip =
new System.Net.IPAddress(bytes);
8214 ipAddress = ip.ToString();
8217 var octets = ipAddress.Split(
'.');
8218 if (octets.Length != 4)
return false;
8219 if (octets.All(o =>
byte.TryParse(o, out
byte value) && value <= 255))
8223 var host = System.Net.Dns.GetHostEntry(ip);
8227 catch (GPALException)
8231 catch (Exception ex)
8233 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{number}][{ip}] doesn't resolve to a hostname.",
this, GPALObjectType.Browser, ex);
8238 catch (GPALException)
8242 catch (Exception ex)
8244 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"[{number}] doesn't resolve to an IP address",
this, GPALObjectType.Browser, ex);
8250 #endregion <Private>
8257 public void Close(
bool killWebDrivers =
false)
8259 InAction(
false,
false);
8262 BrowserLaunched =
null;
8272 if (
true ==
UseOttoMagic &&
true == _scriptsRegistered)
8280 BrowserSettings.DownloadBehaviorSet =
false;
8284 catch (Exception ex)
8286 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"[{BrowserSettings.BrowserType}] could not be tidied up before closing, so it is closed as it is",
this, GPALObjectType.Browser, ex);
8292 CloseOurWindowOnly();
8293 else if (1 == GPAL.Browsers.Count ||
true == killWebDrivers)
8296 BrowserSettings.PuppeteerCommunicator._isRunning =
false;
8299 BrowserHelper.KillAllRunningProcesses(killWebDrivers,
this);
8302 CloseOurWindowOnly();
8308 BrowserHelper.WaitForBrowserToExit(
BrowserSettings, ProfileRestoreWaitMs);
8310 BrowserSettings.PreviousFirefoxPreferences =
null;
8317 BrowserHelper.WaitForBrowserToExit(
BrowserSettings, ProfileRestoreWaitMs);
8319 BrowserSettings.PreviousDownloadPreferences =
null;
8327 BrowserSettings.Process =
null;
8329 GPAL.Browsers.Remove(
this);
8337 private void CloseOurWindowOnly()
8378 internal void ResolveInteractionType(Selector selector)
8380 InteractionType engineDefault =
true ==
UseOttoMagic ? InteractionType.OttoMagic
8382 : InteractionType.Selenium;
8383 InteractionType resolved = engineDefault;
8384 bool wantsHardware = InteractionType.Hardware == selector.RequestedInteractionType ||
true == BrowserSettings.UseHardware ||
true == GPAL.GPALSettings.UseHardware;
8386 if (
true == wantsHardware)
8395 BrowserSettings.HiddenDesktopHardwareReported =
true;
8397 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Hardware input cannot reach [{BrowserSettings.HiddenDesktopName}] while it is hidden, so [{engineDefault}] is used instead. Peek at the desktop to use hardware there.",
this, GPALObjectType.Browser);
8401 resolved = InteractionType.Hardware;
8402 else if (InteractionType.Hardware == selector.RequestedInteractionType)
8403 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Selector [{selector.Name}] asked for hardware interaction, which a headless browser has no mouse or keyboard to perform. Using [{engineDefault}].",
this, GPALObjectType.Browser);
8405 else if (InteractionType.JavaScript == selector.RequestedInteractionType ||
true ==
BrowserSettings.UseJavaScript)
8407 resolved =
true ==
UseOttoMagic ? InteractionType.OttoMagic : InteractionType.JavaScript;
8408 else if (InteractionType.Selenium == selector.RequestedInteractionType &&
false ==
UseSelenium)
8409 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Selector [{selector.Name}] asked for selenium interaction, which the [{AutomationEngine}] engine has no driver for. Using [{engineDefault}].",
this, GPALObjectType.Browser);
8411 selector.InteractionType = resolved;
8419 if (
false == selector.RemovedMethods.Contains(func))
8420 selector.RemovedMethods.Add(func);
8423 if (
false == selector.RemovedMethods.Contains(func))
8424 selector.RemovedMethods.Add(func);
8427 if (
false == selector.RemovedMethods.Contains(func))
8428 selector.RemovedMethods.Add(func);
8462 return (function() {
8464 const overflow = getComputedStyle(document.documentElement).overflow;
8465 if (overflow !== 'hidden') {
8466 return (window.innerHeight + window.scrollY) >= document.body.scrollHeight;
8468 const scrollable = Array.from(document.querySelectorAll('*')).find(
8469 el => el.scrollHeight > el.clientHeight && getComputedStyle(el).overflowY !== 'hidden'
8471 return (scrollable.clientHeight + scrollable.scrollTop) >= scrollable.scrollHeight;
8493 #endregion <Helpers>
8494 #region Window Handling
8499 public IAllowBrowserActionOrAnySelector
NextWindow
8509 BrowserSettings.WindowIdsToURL[window.WindowId] =
new WindowInfo { URL = window.Url, ActiveWindow =
true };
8510 BrowserSettings.CurrentURL = window.Url;
8521 if (trackedWindows.Count <= 1)
8525 string currentHandle = trackedWindows.FirstOrDefault(h =>
8529 if (
string.IsNullOrEmpty(currentHandle))
8533 int currentIndex = trackedWindows.IndexOf(currentHandle);
8534 if (currentIndex == -1) currentIndex = 0;
8537 int nextIndex = (currentIndex + 1) % trackedWindows.Count;
8538 string nextHandle = trackedWindows[nextIndex];
8565 public IAllowBrowserActionOrAnySelector
OpenWindow(GPALUrl URL)
8569 string urlString =
MagicHelper.GetFullUrl(URL?.Url,
this, out _areRobotsAllowed);
8573 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Visiting [{URL?.Url}] is disallowed by robots.txt and your request to honor it. Not going to URL. Workflow will fail.",
this, GPALObjectType.Browser);
8574 URL.ForUrl(
"https://google.com");
8579 WindowTuple window =
MagicHelper.OpenWindow(urlString);
8594 BrowserSettings.CurrentURL = window.Url ?? urlString;
8600 BrowserSettings.CurrentURL = urlString;
8612 tabWindows[newHandle] = newHandle;
8632 URL = GetSetCurrentUrl();
8635 if (
true ==
BrowserSettings.RespectRobotMetaTags &&
false == UrlHelper.CheckRobotMetaTags(URL,
this))
8637 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Visiting [{BrowserSettings.CurrentURL}] is disallowed by robot meta tags and your request to honor them..");
8638 URL.ForUrl(
"https://google.com");
8643 UpdateActiveTab(tabTuple);
8674 WindowTuple window =
MagicHelper.PreviousWindow();
8685 BrowserSettings.CurrentURL = window.Url;
8696 if (trackedWindows.Count <= 1)
8700 string currentHandle = trackedWindows.FirstOrDefault(h =>
8704 if (
string.IsNullOrEmpty(currentHandle))
8708 int currentIndex = trackedWindows.IndexOf(currentHandle);
8709 if (currentIndex == -1) currentIndex = 0;
8712 int previousIndex = (currentIndex - 1 + trackedWindows.Count) % trackedWindows.Count;
8713 string previousHandle = trackedWindows[previousIndex];
8740 public IAllowBrowserActionOrAnySelector
GoToWindow(dynamic urlOrId)
8745 string windowId =
null;
8746 WindowTuple window =
null;
8748 if (urlOrId is GPALUrl url)
8750 windowId =
BrowserSettings.WindowIdsToURL.FirstOrDefault(x => x.Value.URL.Contains(url.Url) || url.Url.Contains(x.Value.URL)).Key;
8751 BrowserSettings.CurrentGPALUrl = url;
8753 else if (urlOrId is
string urlStr)
8754 windowId =
BrowserSettings.WindowIdsToURL.FirstOrDefault(x => x.Value.URL.Contains(urlStr) || urlStr.Contains(x.Value.URL)).Key;
8756 if (
true ==
string.IsNullOrEmpty(windowId))
8764 if (
false ==
BrowserSettings.WindowIdsToURL.ContainsKey(window.WindowId))
8765 BrowserSettings.WindowIdsToURL[window.WindowId] =
new WindowInfo { URL = window.Url, ActiveWindow =
true };
8769 BrowserSettings.CurrentURL = window.Url;
8773 string windowId =
null;
8774 WindowTuple window =
null;
8776 if (urlOrId is GPALUrl url)
8778 windowId =
BrowserSettings.WindowIdsToURL.FirstOrDefault(x => x.Value.URL.Contains(url.Url) || url.Url.Contains(x.Value.URL)).Key;
8779 BrowserSettings.CurrentGPALUrl = url;
8781 else if (urlOrId is
string urlStr)
8782 windowId =
BrowserSettings.WindowIdsToURL.FirstOrDefault(x => x.Value.URL.Contains(urlStr) || urlStr.Contains(x.Value.URL)).Key;
8784 if (
true ==
string.IsNullOrEmpty(windowId))
8786 else if (
true == Int32.TryParse(windowId, out
int windowIdInt))
8787 window =
PuppeteerClient.GoToWindow(windowIdInt).Execute<WindowTuple>();
8792 if (
false ==
BrowserSettings.WindowIdsToURL.ContainsKey(window.WindowId))
8793 BrowserSettings.WindowIdsToURL[window.WindowId] =
new WindowInfo { URL = window.Url, ActiveWindow =
true };
8797 BrowserSettings.CurrentURL = window.Url;
8801 string theUrl =
null;
8802 string targetHandle =
null;
8804 if (urlOrId is GPALUrl url)
8810 if (urlOrId is
string str && (handle == str ||
BrowserDriver.Url.Contains(str)))
8812 targetHandle = handle;
8817 if (targetHandle !=
null)
8824 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to detect window with URL/ID [{urlOrId}]",
this, GPALObjectType.Browser);
8837 public IAllowBrowserActionOrAnySelector
CloseWindow(dynamic urlOrId =
null)
8840 urlOrId =
MagicHelper.GetFullUrl(urlOrId,
this, out
bool _);
8844 BrowserSettings.CurrentGPALUrl =
null;
8848 string windowIdToClose =
null;
8850 if (urlOrId is GPALUrl url)
8852 if (
false ==
string.IsNullOrEmpty(url.Url))
8854 windowIdToClose = BrowserSettings.WindowIdsToURL
8855 .FirstOrDefault(x => x.Value.URL.Contains(url.Url) || url.Url.Contains(x.Value.URL))
8858 else if (urlOrId is
string urlStr)
8860 if (
false ==
string.IsNullOrEmpty(urlStr))
8862 windowIdToClose = BrowserSettings.WindowIdsToURL
8863 .FirstOrDefault(x => x.Value.URL.Contains(urlStr) || urlStr.Contains(x.Value.URL))
8869 windowIdToClose = BrowserSettings.WindowIdsToURL
8870 .FirstOrDefault(x =>
true == x.Value.ActiveWindow)
8874 if (
string.IsNullOrEmpty(windowIdToClose))
8876 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
8877 $
"No window found to close (URL containing [{urlOrId}])",
this, GPALObjectType.Browser);
8882 WindowTuple newActiveWindow =
MagicHelper.CloseWindow(windowIdToClose);
8888 if (newActiveWindow !=
null && !
string.IsNullOrEmpty(newActiveWindow.WindowId))
8893 BrowserSettings.WindowIdsToURL[newActiveWindow.WindowId] =
new WindowInfo
8895 URL = newActiveWindow.Url,
8899 BrowserSettings.CurrentURL = newActiveWindow.Url;
8903 BrowserSettings.CurrentURL =
null;
8909 if (urlOrId is GPALUrl url)
8911 else if (urlOrId is
string urlStr)
8913 else if (urlOrId is
int id)
8920 string handleToClose =
null;
8922 if (urlOrId is
string urlStr)
8925 handleToClose = BrowserSettings.WindowIdsToURL
8926 .FirstOrDefault(x => x.Value.URL.Contains(urlStr) || urlStr.Contains(x.Value.URL))
8932 handleToClose = BrowserSettings.WindowIdsToURL
8933 .FirstOrDefault(x => x.Value.ActiveWindow)
8937 if (
string.IsNullOrEmpty(handleToClose))
8941 if (
string.IsNullOrEmpty(handleToClose) ||
8944 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
8945 $
"No window found to close (URL containing [{urlOrId}])",
this, GPALObjectType.Browser);
8959 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"No windows remain after closing.",
this, GPALObjectType.Browser);
8978 var firstRemaining =
BrowserSettings.WindowIdsToURL.Keys.FirstOrDefault();
8979 if (firstRemaining !=
null)
8991 #endregion Window Handling
8992 #region WithWorkFlow, Run, While, Until, Generated by Grok
8998 public IAllowBrowserActionOrAnySelector
Run()
9000 WorkflowManager.Run();
9037 return WorkflowManager.WithWorkflow(workflow);
9049 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Invalid timeout ([{timeoutMs}]ms) specified. Using default [{BrowserSettings.WhileLoopTimeoutMs}]ms.",
this, GPALObjectType.Browser);
9053 BrowserSettings.WhileLoopTimeoutMs = timeoutMs;
9065 if (maxIterations <= 0)
9067 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"Invalid max iterations ([{maxIterations}]) specified. Using default [{BrowserSettings.WhileLoopMaxIterations}].",
this, GPALObjectType.Browser);
9071 BrowserSettings.WhileLoopMaxIterations = maxIterations;
9084 return WorkflowManager.While(predicate);
9095 return WorkflowManager.Until(selector);
9106 return WorkflowManager.Until(predicate);
9108 #endregion WithWorkFlow, Run, While, Until, Generated by Grok
9111 #region Getters/Setters
9118 internal set => BrowserSettings.RobotsTxt = value;
9126 internal set => BrowserSettings.JavaScriptResultObj = value;
9134 internal set => BrowserSettings.JavaScriptResultStr = value;
9142 internal set => BrowserSettings.Version = value;
9152 internal static string GetModifierKeys(ModifierKeys modifierKeys)
9154 if (modifierKeys == ModifierKeys.NONE)
9157 var setFlags = Enum.GetValues(typeof(ModifierKeys))
9158 .Cast<ModifierKeys>()
9159 .Where(flag => flag != ModifierKeys.NONE && modifierKeys.HasFlag(flag))
9160 .Select(flag => flag.ToString());
9162 return string.Join(
", ", setFlags);
9169 internal string GetSetCurrentUrl()
9171 string currentUrl =
null;
9178 currentUrl =
BrowserSettings.Browser.PuppeteerClient.GetCurrentUrl().Execute<dynamic>();
9184 currentUrl = currentUrl?.Trim(
'"');
9186 if (
false == UrlHelper.AreEquivalent(currentUrl,
BrowserSettings.CurrentURL))
9188 BrowserSettings.CurrentURL = currentUrl;
9191 foreach (Selector selector
in this.CurrentUOW.WithSelectorList)
9193 selector.WebSelectorFoundResults =
null;
9194 selector.WebSelectorMatchedResults =
null;
9198 catch (GPALException)
9202 catch (Exception ex)
9204 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Unable to get URL, returning [{BrowserSettings.CurrentURL}]",
this, GPALObjectType.Browser, ex);
9212 internal void SetReferrer()
9214 string message = $
"[{BrowserSettings.Referrer}] referrer applied.";
9215 var parms =
new Dictionary<string, object>
9218 "headers",
new Dictionary<string, object>
9220 {
"Referer", BrowserSettings.Referrer }
9227 bool applied =
true;
9234 ((OpenQA.Selenium.Chromium.ChromiumDriver)
BrowserDriver).ExecuteCdpCommand(
"Network.setExtraHTTPHeaders", parms);
9241 BrowserSettings.Referrer =
null;
9242 message = $
"Cleared referrer.";
9245 if (
true == applied)
9246 GPAL.PublishSimpleEvent(GPALEventType.INFO, message,
this, GPALObjectType.Browser);
9248 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"[{BrowserSettings.BrowserType}] on [{BrowserSettings.AutomationEngine}] cannot set a header, so the referrer was not applied",
this, GPALObjectType.Browser);
9255 internal void SetUserAgent()
9257 string message = $
"[{BrowserSettings.OverrideUserAgent}] user agent applied.";
9258 var parms =
new Dictionary<string, object> { {
"userAgent", BrowserSettings.OverrideUserAgent } };
9262 bool applied =
true;
9269 ((OpenQA.Selenium.Chromium.ChromiumDriver)
BrowserDriver).ExecuteCdpCommand(
"Network.setUserAgentOverride", parms);
9276 BrowserSettings.OverrideUserAgent =
null;
9277 message = $
"Cleared user agent override.";
9280 if (
true == applied)
9283 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"[{BrowserSettings.BrowserType}] on [{BrowserSettings.AutomationEngine}] cannot set a user agent, so the override was not applied",
this, GPALObjectType.Browser);
9287 #region Sitep Operations
9295 hydrationResult = CachedHydration();
9303 private NextJsHydrationResult CachedHydration()
9305 string key = GetSetCurrentUrl();
9307 if (
false ==
string.IsNullOrEmpty(key) &&
true == _hydrationCache.TryGetValue(key, out NextJsHydrationResult cached))
9309 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Hydrated data cache hit for [{UrlHelper.GetOrigin(CurrentUrl)}]",
this, GPALObjectType.Browser);
9314 NextJsHydrationResult result = NextJsHydrationExtractor.ExtractFromScripts(
this, GetHydrationScripts());
9316 if (
false ==
string.IsNullOrEmpty(key))
9317 _hydrationCache[key] = result;
9336 private string GetHydrationScripts()
9344 reported =
BrowserHelper.ExecuteJavaScriptObj(NextJsHydrationExtractor.HydrationScript,
this)?.ToString();
9347 reported =
BrowserHelper.ExecuteJavaScriptObj(
"return " + NextJsHydrationExtractor.HydrationScript +
";",
this)?.ToString();
9349 return reported ??
string.Empty;
9357 NextJsHydrationExtractor.SaveToFile(CachedHydration(), hydrationFile.
Filename);
9359 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Saved hydrated data [{hydrationFile.Filename}]",
this, GPALObjectType.Browser);
9374 digestResult = CachedDigest(ruleSetName);
9384 string key = GetSetCurrentUrl() +
"|" + (ruleSetName ??
string.Empty);
9386 if (
true == _digestCache.TryGetValue(key, out
LLMDigestResult cached))
9388 GPAL.
PublishSimpleEvent(GPALEventType.DEBUG, $
"LLM Digest cache hit for [{UrlHelper.GetOrigin(CurrentUrl)}]",
this, GPALObjectType.Browser);
9393 LLMDigestResult digestResult = HtmlToOptimizedMarkdown.CreateDigest(
this, pageSource, ruleSetName);
9394 _digestCache[key] = digestResult;
9396 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"LLM Digest created for [{UrlHelper.GetOrigin(CurrentUrl)}]",
this, GPALObjectType.Browser);
9398 return digestResult;
9411 HtmlToOptimizedMarkdown.SaveToFile(CachedDigest(ruleSetName), llmDigestFile.
Filename);
9413 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Saved LLM Digest [{llmDigestFile.Filename}]",
this, GPALObjectType.Browser);
9422 public IAllowBrowserActionOrAnySelector
GetPageSource(out
string pageSource)
9424 pageSource =
string.Empty;
9440 catch (GPALException)
9444 catch (Exception ex)
9446 GPAL.PublishSimpleEvent(
9447 GPALEventType.ERROR,
9448 "Failed to read page source in Selenium [" + ex.Message +
"]",
9450 GPALObjectType.Browser);
9454 pageSource = NormalizePageSource(pageSource);
9456 Uri currUrl =
new Uri(
CurrentUrl, UriKind.Absolute);
9458 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Retrieved page source for [{UrlHelper.NormalizePath(currUrl.AbsolutePath)}]",
this, GPALObjectType.Browser);
9470 private static string NormalizePageSource(
string raw)
9472 if (
string.IsNullOrWhiteSpace(raw))
9473 return string.Empty;
9477 string working = SafeJsUnescape(raw.Trim().Trim(
'\"').Trim(
'\''));
9479 if (working.IndexOf(
"webkit-xml-viewer-source-xml", StringComparison.Ordinal) != -1)
9481 string fileSource = ConverterHelper.ExtractRawXmlFromBrowserSource(working);
9482 if (!
string.IsNullOrWhiteSpace(fileSource))
9486 return Regex.Replace(fileSource.Trim(),
@"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]",
"");
9491 return SafeCleanPageSource(raw);
9498 private static string SafeCleanPageSource(
string input)
9500 if (
string.IsNullOrWhiteSpace(input))
9501 return string.Empty;
9503 string working = input.Trim().Trim(
'\"').Trim(
'\'');
9508 working = SafeJsUnescape(working);
9510 working = System.Web.HttpUtility.HtmlDecode(working);
9513 working = Regex.Replace(working,
@"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]",
"");
9515 return working.Trim();
9523 private static string SafeJsUnescape(
string input)
9525 if (
string.IsNullOrEmpty(input))
9531 string result = TryJsonStringDecode(input) ?? input;
9539 result = result.Replace(
@"\""",
"\"")
9540 .Replace(
@"\'",
"'")
9541 .Replace(
@"\\",
"\\")
9542 .Replace(
@"\/",
"/");
9545 result = Regex.Replace(result,
@"\\u([0-9a-fA-F]{4})", m =>
9549 int code =
int.Parse(m.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
9550 if (code < 0 || code > 0x10FFFF || (code >= 0xD800 && code <= 0xDFFF))
9552 return char.ConvertFromUtf32(code);
9561 result = Regex.Replace(result,
@"\\([^""'\\/bfnrtu])",
"$1");
9573 private static string TryJsonStringDecode(
string s)
9575 if (
string.IsNullOrEmpty(s))
9580 return JsonConvert.DeserializeObject(
"\"" + s +
"\"") as string;
9599 Dictionary<string, List<string>> siteMaps =
new Dictionary<string, List<string>>();
9600 List<string> suggestedSitemaps =
new List<string>();
9606 private readonly Dictionary<string, List<string>> _sitemapCache =
new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
9607 private readonly Dictionary<string, LLMDigestResult> _digestCache =
new Dictionary<string, LLMDigestResult>(StringComparer.OrdinalIgnoreCase);
9608 private readonly Dictionary<string, NextJsHydrationResult> _hydrationCache =
new Dictionary<string, NextJsHydrationResult>(StringComparer.OrdinalIgnoreCase);
9624 public IReadOnlyList<string>
ColumnNames {
get;
private set; } =
new List<string>();
9628 private readonly Dictionary<string, SitemapNote> _sitemapNotes =
new Dictionary<string, SitemapNote>(StringComparer.OrdinalIgnoreCase);
9631 private SitemapNote _lastWalkNote =
null;
9634 private class SitemapNote
9636 internal string Result {
get;
set; }
9637 internal string Message {
get;
set; }
9647 _sitemapCache.Clear();
9648 _sitemapNotes.Clear();
9649 _digestCache.Clear();
9650 _hydrationCache.Clear();
9651 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Cleared sitemap/digest/hydration result cache.",
this, GPALObjectType.Browser);
9666 public IAllowBrowserActionOrAnySelector
GetSiteMapUrls(out List<string> sitemapUrls,
string sitemapUrl =
null)
9668 sitemapUrls = CachedSitemapUrls(sitemapUrl);
9676 private List<string> CachedSitemapUrls(
string sitemapUrl =
null)
9680 string key =
false ==
string.IsNullOrWhiteSpace(sitemapUrl) ? sitemapUrl : UrlHelper.GetOrigin(
CurrentUrl);
9682 if (
false ==
string.IsNullOrEmpty(key) &&
true == _sitemapCache.TryGetValue(key, out List<string> cached))
9684 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Sitemap cache hit for [{key}] ([{cached.Count}] URLs).",
this, GPALObjectType.Browser);
9687 _sitemapNotes.TryGetValue(key, out _lastWalkNote);
9695 List<string> walked = WalkSitemapUrls(sitemapUrl);
9697 if (
false ==
string.IsNullOrEmpty(key))
9699 _sitemapCache[key] = walked;
9701 if (
null == _lastWalkNote)
9702 _sitemapNotes.Remove(key);
9704 _sitemapNotes[key] = _lastWalkNote;
9721 private string ReadSitemap(
string url)
9723 string retVal =
string.Empty;
9729 bool alreadyThere =
true == UrlHelper.AreEquivalent(
CurrentUrl, url);
9741 string onNow = (
CurrentUrl ??
string.Empty).Split(
'?',
'#')[0].TrimEnd(
'/').ToLower();
9743 && (
true == onNow.EndsWith(
".xml") ||
true == onNow.EndsWith(
".txt") ||
true == onNow.EndsWith(
".gz"));
9745 bool sameOrigin =
false == alreadyThere
9746 &&
false == onADocument
9747 &&
true ==
string.Equals(UrlHelper.GetOrigin(
CurrentUrl), UrlHelper.GetOrigin(url), StringComparison.OrdinalIgnoreCase);
9749 byte[] fetched =
true == sameOrigin
9750 ? BrowserHelper.FetchBytes(
this, url,
null, out
string _)
9757 if (
true == answered &&
null != fetched && 0 < fetched.Length)
9758 retVal = System.Text.Encoding.UTF8.GetString(fetched);
9762 bool looksLikeASitemap =
false ==
string.IsNullOrWhiteSpace(retVal)
9763 && (
true == retVal.Contains(
"<urlset")
9764 ||
true == retVal.Contains(
"<sitemapindex")
9765 ||
true == SitemapExtractor.IsValidSitemapContent(retVal));
9769 if (
true == alreadyThere)
9771 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"The browser is already on [{url}], so its page is the sitemap",
this, GPALObjectType.Browser);
9775 else if (
false == looksLikeASitemap)
9777 GPAL.PublishSimpleEvent(GPALEventType.DEBUG,
true == sameOrigin
9778 ? $
"The page did not hand over [{url}], HTTP [{ServerResponseCode}], so the browser goes there to read it"
9779 :
true == onADocument
9780 ? $
"The browser is on [{CurrentUrl}], a document rather than a page, which the extension cannot fetch from, so it goes to [{url}] instead"
9781 : $
"[{url}] is not on the origin the page is on, which a page may not fetch, so the browser goes there to read it",
9782 this, GPALObjectType.Browser);
9791 private List<string> WalkSitemapUrls(
string sitemapUrl =
null)
9793 List<string> sitemapUrls =
new List<string>();
9800 string currentUrl =
false ==
string.IsNullOrWhiteSpace(sitemapUrl) ? sitemapUrl : startedOn;
9801 bool isFromRobotsTxt =
false;
9802 SitemapExtractor sme =
new SitemapExtractor(
this);
9803 List<string> potentialSitemapUrls = sme.GetSitemapUrl(currentUrl, out isFromRobotsTxt);
9804 bool urlIsSitemap =
false;
9805 IGPALGrid<string> retGrid =
new GPALGrid<string>();
9807 string pageSource =
string.Empty;
9809 _lastWalkNote =
null;
9813 string potentialSitemapUrl =
string.Empty;
9814 if (idx < potentialSitemapUrls.Count)
9815 potentialSitemapUrl = potentialSitemapUrls[idx++];
9820 if (
false == isFromRobotsTxt)
9823 if (
true == SitemapExtractor.IsGzipSitemapUrl(currentUrl))
9824 pageSource = SitemapExtractor.FetchAndInflateGzipSitemap(currentUrl,
this);
9826 pageSource = ReadSitemap(potentialSitemapUrl);
9832 if (
true == isFromRobotsTxt ||
9833 (
false == currentUrl.ToLower().Contains(
"sitemap") &&
9834 false == currentUrl.ToLower().EndsWith(
".xml") &&
9835 false == SitemapExtractor.IsValidSitemapContent(pageSource) &&
9836 false == suggestedSitemaps.Contains(potentialSitemapUrl)
9841 if (
true == SitemapExtractor.IsGzipSitemapUrl(potentialSitemapUrl))
9842 pageSource = SitemapExtractor.FetchAndInflateGzipSitemap(potentialSitemapUrl,
this);
9844 pageSource = ReadSitemap(potentialSitemapUrl);
9846 if (
false == suggestedSitemaps.Contains(potentialSitemapUrl))
9847 suggestedSitemaps.Add(potentialSitemapUrl);
9851 if (
true == SitemapExtractor.IsBotCheckPage(pageSource))
9853 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Bot-check interstitial detected at [{currentUrl}], skipping sitemap walk.",
this, GPALObjectType.Browser);
9855 _lastWalkNote =
new SitemapNote
9857 Result =
"bot-check-page",
9858 Message = $
"A bot-check interstitial answered for [{currentUrl}] instead of a sitemap, so the walk stopped there and no urls were read. Run this one on a signed-in profile, or read the sitemap the site publishes by name."
9864 sitemapUrls = sme.ExtractSitemapUrls(pageSource, Int32.MaxValue, out urlIsSitemap);
9865 retGrid.AddRow(sitemapUrls);
9866 string currentUrl2 =
string.Empty;
9875 currentUrl2 = currentUrl2.Trim(
'\"');
9880 if (0 == potentialSitemapUrls.Count || potentialSitemapUrls.Count == idx)
9882 if (
false == UrlHelper.AreEquivalent(startedOn, currentUrl2))
9889 sitemapUrls = retGrid.ToList();
9898 if (0 == sitemapUrls.Count &&
null == _lastWalkNote)
9900 string tried = 0 == potentialSitemapUrls.Count
9901 ?
"nothing: robots.txt named no sitemap and the page is not one"
9902 :
string.Join(
", ", potentialSitemapUrls);
9904 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"No sitemap URLs found for [{currentUrl}], tried [{tried}]",
this, GPALObjectType.Browser);
9906 _lastWalkNote =
new SitemapNote
9908 Result =
"no-sitemap-found",
9909 Message = $
"No sitemap was found for [{currentUrl}], so no urls were walked. Tried [{tried}]."
9912 else if (0 < sitemapUrls.Count)
9913 _lastWalkNote =
null;
9926 List<string> sitemapUrls = CachedSitemapUrls();
9929 IGPALGrid<string> grid =
new GPALGrid<string>();
9931 if (0 == sitemapUrls.Count)
9936 SitemapNote note = _lastWalkNote ??
new SitemapNote
9938 Result =
"no-sitemap-found",
9939 Message = $
"No sitemap was found for [{UrlHelper.GetOrigin(CurrentUrl)}], so there are no urls to write."
9942 grid.AddRow(
new List<string> { note.Result, note.Message });
9946 .WithColumnName(
"result")
9947 .WithColumnName(
"message")
9948 .SaveTo(sitemapUrlsFile);
9950 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"No sitemap URLs to save, so [{sitemapUrlsFile.Filename}] says [{note.Result}] instead",
this, GPALObjectType.Browser);
9954 foreach (
string url
in sitemapUrls)
9955 grid.AddRow(
new List<string> { url });
9959 .WithColumnName(
"url")
9960 .SaveTo(sitemapUrlsFile);
9962 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Saved [{sitemapUrls.Count}] sitemap URLs to [{sitemapUrlsFile.Filename}]",
this, GPALObjectType.Browser);
9967 #endregion Sitemap Operations
9968 #region Cast Operations
9972 private string SinkName {
get;
set; }
9982 SinkName = sinkName;
9988 CastTo_Selenium(sinkName, CastMode.Tab);
10003 SinkName = sinkName;
10009 CastTo_Selenium(sinkName, CastMode.Desktop);
10020 public IAllowBrowserActionOrAnySelector
StopCasting()
10022 if (
false ==
string.IsNullOrEmpty(SinkName))
10031 PuppeteerCommunicator.SinkName =
null;
10036 var driver = (OpenQA.Selenium.Chromium.ChromiumDriver)
BrowserDriver;
10038 var param =
new Dictionary<string, object> { [
"sinkName"] = SinkName };
10040 if (
null != SinkName)
10044 driver.ExecuteCdpCommand(
"Cast.stopCasting", param);
10047 catch (Exception ex)
10049 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"Cast.stopCasting failed, which it does sometimes",
this, GPALObjectType.Browser, ex);
10063 private IAllowBrowserActionOrAnySelector CastTo_Selenium(
string sinkName, CastMode mode)
10067 if (
string.IsNullOrEmpty(sinkName))
10070 ((OpenQA.Selenium.Chromium.ChromiumDriver)
BrowserDriver).ExecuteCdpCommand(
"Cast.enable",
10071 new Dictionary<string, object>());