/// <summary> /// Scans a list for "modern" compatibility /// </summary> /// <param name="file">List form page to start the scan from</param> /// <param name="list">List linked to the form page</param> /// <returns>Object describing modern compatiblity</returns> public static ListScanResult ModernCompatability(this File file, List list, ref ConcurrentStack <ScanError> scanErrors) { if (file == null) { throw new ArgumentNullException("file"); } if (list == null) { throw new ArgumentNullException("list"); } ClientContext cc = file.Context as ClientContext; ListScanResult result = new ListScanResult(); // Load properties file.EnsureProperties(p => p.PageRenderType); // If it works in modern, we're good if (file.PageRenderType == ListPageRenderType.Modern) { // let's return since we know it will work return(result); } else { result.PageRenderType = file.PageRenderType; } // Hmmm...it's not working, so let's list *all* reasons why it's not working in modern // Step 1: load the tenant / site / web / list level blocking options // Tenant // Currently we've no API to detect tenant setting...but since we anyhow should scan all lists this does not matter that much // Site Site site = cc.Site; site.EnsureProperties(p => p.Features, p => p.Url); result.BlockedAtSiteLevel = site.Features.Where(f => f.DefinitionId == FeatureId_Site_Modern).Count() > 0; // Web cc.Web.EnsureProperties(p => p.Features, p => p.Url); result.BlockedAtWebLevel = cc.Web.Features.Where(f => f.DefinitionId == FeatureId_Web_Modern).Count() > 0; // List list.EnsureProperties(p => p.ListExperienceOptions, p => p.UserCustomActions, p => p.BaseTemplate); result.ListExperience = list.ListExperienceOptions; result.XsltViewWebPartCompatibility.ListBaseTemplate = list.BaseTemplate; if (list.ListExperienceOptions == ListExperience.ClassicExperience) { result.BlockedAtListLevel = true; } // Step 2: verify we can load a web part manager and ensure there's only one web part of the page LimitedWebPartManager wpm; try { wpm = file.GetLimitedWebPartManager(PersonalizationScope.Shared); file.Context.Load(wpm.WebParts, wps => wps.Include(wp => wp.WebPart.Title, wp => wp.WebPart.Properties)); file.Context.ExecuteQueryRetry(); } catch (Exception ex) { result.BlockedByNotBeingAbleToLoadPage = true; result.BlockedByNotBeingAbleToLoadPageException = ex.ToString(); return(result); } if (wpm.WebParts.Count != 1) { result.BlockedByZeroOrMultipleWebParts = true; return(result); } var webPart = wpm.WebParts[0].WebPart; // Step 3: Inspect the web part used to render the list // Step 3.1: JSLink web part check if (webPart.Properties.FieldValues.Keys.Contains("JSLink")) { if (webPart.Properties["JSLink"] != null && !String.IsNullOrEmpty(webPart.Properties["JSLink"].ToString()) && webPart.Properties["JSLink"].ToString().ToLower() != "clienttemplates.js") { result.XsltViewWebPartCompatibility.BlockedByJSLink = true; result.XsltViewWebPartCompatibility.JSLink = webPart.Properties["JSLink"].ToString(); } } // Step 3.2: XslLink web part check if (webPart.Properties.FieldValues.Keys.Contains("XslLink")) { if (webPart.Properties["XslLink"] != null && !String.IsNullOrEmpty(webPart.Properties["XslLink"].ToString()) && webPart.Properties["XslLink"].ToString().ToLower() != "main.xsl") { result.XsltViewWebPartCompatibility.BlockedByXslLink = true; result.XsltViewWebPartCompatibility.XslLink = webPart.Properties["XslLink"].ToString(); } } // Step 3.3: Xsl web part check if (webPart.Properties.FieldValues.Keys.Contains("Xsl")) { if (webPart.Properties["Xsl"] != null && !String.IsNullOrEmpty(webPart.Properties["Xsl"].ToString())) { result.XsltViewWebPartCompatibility.BlockedByXsl = true; } } // Step 3.4: Process fields in view if (webPart.Properties.FieldValues.Keys.Contains("XmlDefinition")) { if (webPart.Properties["XmlDefinition"] != null && !String.IsNullOrEmpty(webPart.Properties["XmlDefinition"].ToString())) { try { // Get the fields in this view var viewFields = GetViewFields(webPart.Properties["XmlDefinition"].ToString()); // Load fields in one go List <Field> fieldsToProcess = new List <Field>(viewFields.Count); try { foreach (var viewField in viewFields) { Field field = list.Fields.GetByInternalNameOrTitle(viewField); cc.Load(field, p => p.JSLink, p => p.TypeAsString, p => p.FieldTypeKind, p => p.InternalName); fieldsToProcess.Add(field); } cc.ExecuteQueryRetry(); } catch { // try to load the fields again, but now individually so we can collect the needed errors + evaulate the fields that do load fieldsToProcess.Clear(); foreach (var viewField in viewFields) { try { Field field = list.Fields.GetByInternalNameOrTitle(viewField); cc.Load(field, p => p.JSLink, p => p.TypeAsString, p => p.FieldTypeKind); cc.ExecuteQueryRetry(); fieldsToProcess.Add(field); } catch (Exception ex) { ScanError error = new ScanError() { Error = ex.Message, SiteURL = cc.Web.Url, SiteColUrl = site.Url }; scanErrors.Push(error); Console.WriteLine("Error for site {1}: {0}", ex.Message, cc.Web.Url); } } } // Verify the fields foreach (var field in fieldsToProcess) { try { // JSLink on field if (!string.IsNullOrEmpty(field.JSLink) && field.JSLink != "clienttemplates.js" && field.JSLink != "sp.ui.reputation.js" && !field.IsTaxField()) { result.XsltViewWebPartCompatibility.BlockedByJSLinkField = true; result.XsltViewWebPartCompatibility.JSLinkFields = string.IsNullOrEmpty(result.XsltViewWebPartCompatibility.JSLinkFields) ? $"{field.InternalName}" : $"{result.XsltViewWebPartCompatibility.JSLinkFields},{field.InternalName}"; } //Business data field if (field.IsBusinessDataField()) { result.XsltViewWebPartCompatibility.BlockedByBusinessDataField = true; result.XsltViewWebPartCompatibility.BusinessDataFields = string.IsNullOrEmpty(result.XsltViewWebPartCompatibility.BusinessDataFields) ? $"{field.InternalName}" : $"{result.XsltViewWebPartCompatibility.BusinessDataFields},{field.InternalName}"; } // Geolocation field if (field.FieldTypeKind == FieldType.Geolocation) { result.XsltViewWebPartCompatibility.BlockedByGeoLocationField = true; result.XsltViewWebPartCompatibility.GeoLocationFields = string.IsNullOrEmpty(result.XsltViewWebPartCompatibility.GeoLocationFields) ? $"{field.InternalName}" : $"{result.XsltViewWebPartCompatibility.GeoLocationFields},{field.InternalName}"; } // TaskOutcome field if (field.IsTaskOutcomeField()) { result.XsltViewWebPartCompatibility.BlockedByTaskOutcomeField = true; result.XsltViewWebPartCompatibility.TaskOutcomeFields = string.IsNullOrEmpty(result.XsltViewWebPartCompatibility.TaskOutcomeFields) ? $"{field.InternalName}" : $"{result.XsltViewWebPartCompatibility.TaskOutcomeFields},{field.InternalName}"; } // Publishing field if (field.IsPublishingField()) { result.XsltViewWebPartCompatibility.BlockedByPublishingField = true; result.XsltViewWebPartCompatibility.PublishingFields = string.IsNullOrEmpty(result.XsltViewWebPartCompatibility.PublishingFields) ? $"{field.InternalName}" : $"{result.XsltViewWebPartCompatibility.PublishingFields},{field.InternalName}"; } } catch (Exception ex) { ScanError error = new ScanError() { Error = ex.Message, SiteURL = cc.Web.Url, SiteColUrl = site.Url, }; scanErrors.Push(error); Console.WriteLine("Error for site {1}: {0}", ex.Message, cc.Web.Url); } } } catch (Exception ex) { ScanError error = new ScanError() { Error = ex.Message, SiteURL = cc.Web.Url, SiteColUrl = site.Url }; scanErrors.Push(error); Console.WriteLine("Error for site {1}: {0}", ex.Message, cc.Web.Url); } } } // Step 3.5: Process list custom actions if (list.UserCustomActions.Count > 0) { foreach (var customAction in list.UserCustomActions) { if (!string.IsNullOrEmpty(customAction.Location) && customAction.Location.Equals("scriptlink", StringComparison.InvariantCultureIgnoreCase)) { if (!string.IsNullOrEmpty(customAction.ScriptSrc)) { result.XsltViewWebPartCompatibility.BlockedByListCustomAction = true; result.XsltViewWebPartCompatibility.ListCustomActions = string.IsNullOrEmpty(result.XsltViewWebPartCompatibility.ListCustomActions) ? $"{customAction.Name}" : $"{result.XsltViewWebPartCompatibility.ListCustomActions},{customAction.Name}"; } } } } // Step 3.6: managed metadata navigation is not an issue anymore result.XsltViewWebPartCompatibility.BlockedByManagedMetadataNavFeature = false; // Step 4: check the view if (webPart.Properties.FieldValues.Keys.Contains("ViewFlags") && webPart.Properties["ViewFlags"] != null && !String.IsNullOrEmpty(webPart.Properties["ViewFlags"].ToString())) { uint flags; if (uint.TryParse(webPart.Properties["ViewFlags"].ToString(), out flags)) { if ((flags & ViewFlag_Gantt) != 0 || (flags & ViewFlag_Calendar) != 0 || (flags & ViewFlag_Grid) != 0) { result.XsltViewWebPartCompatibility.BlockedByViewType = true; if ((flags & ViewFlag_Gantt) != 0) { result.XsltViewWebPartCompatibility.ViewType = "Gantt"; } else if ((flags & ViewFlag_Calendar) != 0) { result.XsltViewWebPartCompatibility.ViewType = "Calendar"; } else if ((flags & ViewFlag_Grid) != 0) { if (list.BaseTemplate == (int)ListTemplateType.GenericList || list.BaseTemplate == (int)ListTemplateType.DocumentLibrary) { // unblock...we've added support for datasheet rendering for custom lists in July 2018 (see https://techcommunity.microsoft.com/t5/Microsoft-SharePoint-Blog/Updates-to-metadata-handling-and-list-templates/ba-p/202113) result.XsltViewWebPartCompatibility.BlockedByViewType = false; } else { result.XsltViewWebPartCompatibility.ViewType = "Grid"; } } } } } // Step 5: check the list // Step 5.1: check the base template if (!list.CanRenderNewExperience()) { result.XsltViewWebPartCompatibility.BlockedByListBaseTemplate = true; result.XsltViewWebPartCompatibility.ListBaseTemplate = list.BaseTemplate; } return(result); }
/// <summary> /// Analyze the web /// </summary> /// <param name="cc">ClientContext of the web to be analyzed</param> /// <returns>Duration of the analysis</returns> public override TimeSpan Analyze(ClientContext cc) { try { base.Analyze(cc); var baseUri = new Uri(this.SiteUrl); var webAppUrl = baseUri.Scheme + "://" + baseUri.Host; var lists = cc.Web.GetListsToScan(); foreach (var list in lists) { try { this.ScanJob.IncreaseScannedLists(); ListScanResult listScanData; if (list.DefaultViewUrl.ToLower().Contains(".aspx")) { File file = cc.Web.GetFileByServerRelativeUrl(list.DefaultViewUrl); listScanData = file.ModernCompatability(list, ref this.ScanJob.ScanErrors); } else { listScanData = new ListScanResult() { BlockedByNotBeingAbleToLoadPage = true }; } if (listScanData != null && !listScanData.WorksInModern) { if (this.ScanJob.ExcludeListsOnlyBlockedByOobReasons && listScanData.OnlyBlockedByOOBReasons) { continue; } listScanData.SiteURL = this.SiteUrl; listScanData.ListUrl = $"{webAppUrl}{list.DefaultViewUrl}"; listScanData.SiteColUrl = this.SiteCollectionUrl; listScanData.ListTitle = list.Title; if (!this.ScanJob.ListScanResults.TryAdd($"{Guid.NewGuid().ToString()}{webAppUrl}{list.DefaultViewUrl}", listScanData)) { ScanError error = new ScanError() { Error = $"Could not add list scan result for {webAppUrl}{list.DefaultViewUrl} from web scan of {this.SiteUrl}", SiteColUrl = this.SiteCollectionUrl, SiteURL = this.SiteUrl, Field1 = "ListAnalyzer", Field2 = $"{webAppUrl}{list.DefaultViewUrl}" }; this.ScanJob.ScanErrors.Push(error); } } } catch (Exception ex) { ScanError error = new ScanError() { Error = ex.Message, SiteColUrl = this.SiteCollectionUrl, SiteURL = this.SiteUrl, Field1 = "MainListAnalyzerLoop", Field2 = ex.StackTrace, Field3 = $"{webAppUrl}{list.DefaultViewUrl}" }; // Send error to telemetry to make scanner better if (this.ScanJob.ScannerTelemetry != null) { this.ScanJob.ScannerTelemetry.LogScanError(ex, error); } this.ScanJob.ScanErrors.Push(error); Console.WriteLine("Error for page {1}: {0}", ex.Message, $"{webAppUrl}{list.DefaultViewUrl}"); } } } finally { this.StopTime = DateTime.Now; } // return the duration of this scan return(new TimeSpan((this.StopTime.Subtract(this.StartTime).Ticks))); }