void ContentTypeRetentionEnforcementJob_TimerJobRun(object sender, TimerJobRunEventArgs e) { try { Log.Info("ContentTypeRetentionEnforcementJob", "Scanning web {0}", e.Url); //Get all document libraries. Lists are excluded. var documentLibraries = GetAllDocumentLibrariesInWeb(e.WebClientContext, e.WebClientContext.Web); //Iterate through all document libraries foreach (var documentLibrary in documentLibraries) { Log.Info("ContentTypeRetentionEnforcementJob", "Scanning library {0}", documentLibrary.Title); //Iterate through configured content type retention policies in app.config foreach (var contentTypeName in configContentTypeRetentionPolicyPeriods.Keys) { var retentionPeriods = configContentTypeRetentionPolicyPeriods.GetValues(contentTypeName as string); if (retentionPeriods != null) { var retentionPeriod = int.Parse(retentionPeriods[0]); ApplyRetentionPolicy(e.WebClientContext, documentLibrary, contentTypeName, retentionPeriod); } } } } catch (Exception ex) { Log.Error("ContentTypeRetentionEnforcementJob", "Exception processing site {0}. Exception is {1}", e.Url, ex.Message); } }
private void ExecuteProvisioningJobs(object sender, TimerJobRunEventArgs e) { Console.WriteLine("Starting job"); // Show the current context Web web = e.SiteClientContext.Web; web.EnsureProperty(w => w.Title); Console.WriteLine("Processing jobs in Site: {0}", web.Title); // Retrieve the list of pending jobs var provisioningJobs = ProvisioningRepositoryFactory.Current.GetTypedProvisioningJobs <ProvisioningJob>( ProvisioningJobStatus.Pending); foreach (var job in provisioningJobs) { Console.WriteLine("Processing job: {0} - Owner: {1} - Title: {2}", job.JobId, job.Owner, job.Title); Type jobType = job.GetType(); if (PnPPartnerPackSettings.ScheduledJobHandlers.ContainsKey(jobType)) { PnPPartnerPackSettings.ScheduledJobHandlers[jobType].RunJob(job); } } Console.WriteLine("Ending job"); }
private void EnforceTwoAdministratorsTimerJob_TimerJobRun(object sender, TimerJobRunEventArgs e) { Console.WriteLine("Starting job"); var web = e.SiteClientContext.Web; var siteUsers = e.SiteClientContext.LoadQuery(web.SiteUsers.Include(u => u.Email).Where(u => u.IsSiteAdmin)); e.SiteClientContext.ExecuteQueryRetry(); if (siteUsers.Count() < 2) { Console.WriteLine("Site found"); if (!web.IsPropertyAvailable("Url")) { e.SiteClientContext.Load(web, w => w.Url); e.SiteClientContext.ExecuteQueryRetry(); } var adminUser = siteUsers.FirstOrDefault(); EmailProperties mailProps = new EmailProperties(); mailProps.Subject = "Action required: assign an additional site administrator to your site"; StringBuilder bodyBuilder = new StringBuilder(); bodyBuilder.Append("<html><body style=\"font-family:sans-serif\">"); bodyBuilder.AppendFormat("<p>Your site with address <a href=\"{0}\">{0}</a> has only one site administrator defined: you. Please assign an additional site administrator.</p>", e.SiteClientContext.Web.Url); bodyBuilder.AppendFormat("<p>Click here to <a href=\"{0}/_layouts/mngsiteadmin.aspx\">assign an additional site collection adminstrator.</a></p>", e.SiteClientContext.Web.Url); bodyBuilder.Append("</body></html>"); mailProps.Body = bodyBuilder.ToString(); mailProps.To = new[] { adminUser.Email }; Utility.SendEmail(e.SiteClientContext, mailProps); e.SiteClientContext.ExecuteQueryRetry(); } Console.WriteLine("Ending job"); }
/// <summary> /// Load the latest site collection status from SharePoint /// </summary> /// <param name="e">Timer job event arguments</param> /// <param name="tenant">The tenant object</param> /// <param name="site">The site object</param> /// <param name="properties">The site properties object</param> private void LoadSiteStatus(TimerJobRunEventArgs e, out Tenant tenant, out Site site, out SiteProperties properties) { var tenantClientContext = e.TenantClientContext; Log.Info(base.Name, TimerJobsResources.SynchJob_GetSiteStatus, e.Url); tenant = new Tenant(tenantClientContext); site = tenant.GetSiteByUrl(e.Url); properties = tenant.GetSitePropertiesByUrl(e.Url, includeDetail: false); tenantClientContext.Load(tenant, t => t.SharingCapability); tenantClientContext.Load(site, s => s.RootWeb.Title, s => s.RootWeb.Description, s => s.RootWeb.Language, s => s.RootWeb.Created, s => s.Id); tenantClientContext.Load(properties, s => s.StorageMaximumLevel, s => s.StorageWarningLevel, s => s.UserCodeMaximumLevel, s => s.UserCodeWarningLevel, s => s.TimeZoneId, s => s.SharingCapability); tenantClientContext.ExecuteQueryRetry(); }
private void TestTimerJob_TimerJobRun(object sender, TimerJobRunEventArgs e) { var web = e.SiteClientContext.Web; var users = e.SiteClientContext.LoadQuery(web.SiteUsers.Include(uc => uc.Email).Where(uc => uc.IsSiteAdmin)); e.SiteClientContext.ExecuteQueryRetry(); }
void SiteGovernanceJob_TimerJobRun(object o, TimerJobRunEventArgs e) { try { string library = ""; // Get the number of admins var admins = e.WebClientContext.Web.GetAdministrators(); Log.Info("SiteGovernanceJob", "ThreadID = {2} | Site {0} has {1} administrators.", e.Url, admins.Count, Thread.CurrentThread.ManagedThreadId); // grab reference to list library = "SiteAssets"; List list = e.WebClientContext.Web.GetListByUrl(library); if (!e.GetProperty("ScriptFileVersion").Equals("1.0", StringComparison.InvariantCultureIgnoreCase)) { if (list == null) { // grab reference to list library = "Style%20Library"; list = e.WebClientContext.Web.GetListByUrl(library); } if (list != null) { // upload js file to list list.RootFolder.UploadFile("sitegovernance.js", "sitegovernance.js", true); e.SetProperty("ScriptFileVersion", "1.0"); } } if (admins.Count < 2) { // Oops, we need at least 2 site collection administrators e.WebClientContext.Site.AddJsLink(SiteGovernanceJobKey, BuildJavaScriptUrl(e.Url, library)); Console.WriteLine("Site {0} marked as incompliant!", e.Url); e.SetProperty("SiteCompliant", "false"); } else { // We're all good...let's remove the notification e.WebClientContext.Site.DeleteJsLink(SiteGovernanceJobKey); Console.WriteLine("Site {0} is compliant", e.Url); e.SetProperty("SiteCompliant", "true"); } e.CurrentRunSuccessful = true; e.DeleteProperty("LastError"); } catch (Exception ex) { Log.Error("SiteGovernanceJob", "Error while processing site {0}. Error = {1}", e.Url, ex.Message); e.CurrentRunSuccessful = false; e.SetProperty("LastError", ex.Message); } }
/// <summary> /// Timer Job worker method /// </summary> /// <param name="sender">The event sender</param> /// <param name="e">The timer job argument</param> protected override void TimerJobRunImpl(object sender, TimerJobRunEventArgs e) { // build site information from database or create a new one var siteInformation = GetSiteInformation(e.Url); // load the current site status from SPO Tenant tenant; Site site; SiteProperties properties; LoadSiteStatus(e, out tenant, out site, out properties); // update the site information object with updated site status siteInformation.Guid = site.Id; siteInformation.Title = site.RootWeb.Title; siteInformation.Description = site.RootWeb.Description; siteInformation.Lcid = (int)site.RootWeb.Language; siteInformation.CreatedDate = site.RootWeb.Created; siteInformation.StorageMaximumLevel = properties.StorageMaximumLevel; siteInformation.StorageWarningLevel = properties.StorageWarningLevel; siteInformation.UserCodeMaximumLevel = properties.UserCodeMaximumLevel; siteInformation.UserCodeWarningLevel = properties.UserCodeWarningLevel; siteInformation.TimeZoneId = properties.TimeZoneId; siteInformation.SharingStatus = IsExternalSharingEnabled(properties.SharingCapability, tenant.SharingCapability); // update site collection administrators var admins = site.RootWeb.GetAdministrators(); siteInformation.Administrators = (from a in admins select new SiteUser() { LoginName = a.LoginName, Email = a.Email, }).ToList(); // update external users if (siteInformation.SharingStatus != 0) { var externalUsers = site.RootWeb.GetExternalUsersForSiteTenant(new Uri(e.Url)); siteInformation.ExternalUsers = (from u in externalUsers select new ExternalSiteUser() { LoginName = u.AcceptedAs, Email = u.InvitedAs, ExternalUser_AcceptedAs = u.AcceptedAs, ExternalUser_CreatedDate = u.WhenCreated, ExternalUser_DisplayName = u.DisplayName, ExternalUser_InvitedAs = u.InvitedAs, ExternalUser_InvitedBy = u.InvitedBy, ExternalUser_UniqueId = u.UniqueId, } as SiteUser).ToList(); } // write site information into database Log.Info(base.Name, TimerJobsResources.SynchJob_UpdateDbRecord, e.Url); DbRepository.UsingContext(dbContext => { dbContext.SaveSite(siteInformation); }); }
/// <summary> /// Run preprocess for the current site /// </summary> /// <param name="sender">The current timer job instance</param> /// <param name="e">The timer job run event arguments</param> protected override void TimerJobRunImpl(object sender, TimerJobRunEventArgs e) { DbRepository.UsingContext(dbContext => { var web = dbContext.GetWeb(e.Url); var site = dbContext.GetSite(web.SiteUrl); Policy.Preprocess(site, web, e); dbContext.SaveChanges(); }); }
void TenantAPIJob_TimerJobRun(object sender, TimerJobRunEventArgs e) { Tenant t = new Tenant(e.TenantClientContext); var sites = t.GetSiteProperties(0, true); e.TenantClientContext.Load(sites); e.TenantClientContext.ExecuteQueryRetry(); foreach (var site in sites) { Console.WriteLine(site.Template); } }
/// <summary> /// Remove database record if the site is not existing in SharePoint /// </summary> /// <param name="sender">The current time job instance</param> /// <param name="e">Time job event arguments</param> protected override void TimerJobRunImpl(object sender, TimerJobRunEventArgs e) { var tenant = new Tenant(e.TenantClientContext); bool existed = tenant.SiteExists(e.Url); if (existed) { return; } Log.Info(TimerJobsResources.CleanUpJob_RemoveSite, e.Url); DbRepository.UsingContext(context => { var site = context.GetSite(e.Url); context.Sites.Remove(site); }); }
void ExpandJob_TimerJobRun(object sender, TimerJobRunEventArgs e) { // Read the title from the site being processed e.WebClientContext.Load(e.WebClientContext.Web, p => p.Title); e.WebClientContext.ExecuteQueryRetry(); // Read the title from the root site of the site being processed e.SiteClientContext.Load(e.SiteClientContext.Web, p => p.Title); e.SiteClientContext.ExecuteQueryRetry(); Console.WriteLine("Root site of site {0} has title {1}", e.Url, e.SiteClientContext.Web.Title); Console.WriteLine("Sub site {0} has title {1}", e.Url, e.WebClientContext.Web.Title); // Show some threading information ThreadingDebugInformation(); }
/// <summary> /// Timer job run event handler to report the current job progress to console /// </summary> /// <param name="sender">The current timer job instnace</param> /// <param name="e">Timer job run event arguments</param> protected void ManagementTimerJob_TimerJobRun(object sender, TimerJobRunEventArgs e) { try { TimerJobRunImpl(sender, e); } catch (Exception exception) { Console.WriteLine(TimerJobsResources.TenantJob_SiteError, Name, e.Url); Console.WriteLine(exception.Message); throw; } finally { e.TenantClientContext.Dispose(); // Dispose the tenant client context object Console.WriteLine(TimerJobsResources.TenantJob_Progress, ++CompletedSites, TotalSites, Name); } }
/// <summary> /// Run policy checking and enforcement for the current site colleciton /// </summary> /// <param name="sender">The current timer job instance</param> /// <param name="e">Time job run event arguments</param> protected override void TimerJobRunImpl(object sender, TimerJobRunEventArgs e) { SiteInformation site = null; DbRepository.UsingContext(dbContext => { site = dbContext.GetSite(e.Url); if (site == null) { return; } PolicyManager.Run(e.TenantClientContext, site, SuppressEmail); if (site.ComplianceState.DeleteDate == DateTime.MaxValue) { dbContext.Sites.Remove(site); } dbContext.SaveChanges(); }); }
void SiteCollectionScopedJob_TimerJobRun(object sender, TimerJobRunEventArgs e) { // Get all the sub sites in the site we're processing IEnumerable <string> expandedSites = GetAllSubSites(e.SiteClientContext.Site); // Manually iterate over the content foreach (string site in expandedSites) { // Clone the existing ClientContext for the sub web using (ClientContext ccWeb = e.SiteClientContext.Clone(site)) { // Here's the timer job logic, but now a single site collection is handled in a single thread which // allows for further optimization or prevents race conditions ccWeb.Load(ccWeb.Web, s => s.Title); ccWeb.ExecuteQueryRetry(); Console.WriteLine("Here: {0} - {1}", site, ccWeb.Web.Title); } } }
private void PolicyJob_TimerJobRun(object sender, TimerJobRunEventArgs e) { try { Console.WriteLine($"Processing site {e.Url}"); ProvisioningTemplateApplyingInformation ptai = new ProvisioningTemplateApplyingInformation() { HandlersToProcess = Handlers.SiteSecurity, }; XMLTemplateProvider provider = new XMLFileSystemTemplateProvider(@".", ""); ProvisioningTemplate sourceTemplate = provider.GetTemplate(ProvisioningTemplateToApply); e.WebClientContext.Web.ApplyProvisioningTemplate(sourceTemplate, ptai); } catch (Exception ex) { // Catch exceptions to avoid ending the program run since we're typically processing multiple site collections Console.WriteLine($"Error: {ex.Message}"); } }
void Job_TimerJobRun(object sender, TimerJobRunEventArgs e) { Console.WriteLine("----- Timer Job Triggering ---"); try { XmlReaderSettings settings = new XmlReaderSettings(); settings.DtdProcessing = DtdProcessing.Parse; e.WebClientContext.Load(e.WebClientContext.Web); e.WebClientContext.ExecuteQueryRetry(); Web web = e.WebClientContext.Web; Console.WriteLine(web.Title); e.WebClientContext.Load(web); e.WebClientContext.ExecuteQuery(); Console.WriteLine("Site Request"); List list = web.Lists.GetByTitle("Site Request"); e.WebClientContext.Load(list); e.WebClientContext.ExecuteQuery(); GetSiteCollectionDetails(e.WebClientContext, list); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } }
/// <summary> /// Event handler that's being executed by the threads processing the sites. Everything in here must be coded in a thread-safe manner /// </summary> private void SBScanner_TimerJobRun(object sender, TimerJobRunEventArgs e) { lock (scannedSitesLock) { ScannedSites++; } Console.WriteLine("Processing site {0}...", e.Url); try { if (!firstSiteCollectionDone) { firstSiteCollectionDone = true; // Telemetry e.WebClientContext.ClientTag = "SPDev:SBScanner"; e.WebClientContext.Load(e.WebClientContext.Web, p => p.Description); e.WebClientContext.ExecuteQuery(); } // Query the solution gallery CamlQuery camlQuery = CamlQuery.CreateAllItemsQuery(); ListItemCollection itemCollection = e.WebClientContext.Web.GetCatalog(121).GetItems(camlQuery); e.WebClientContext.Load(e.WebClientContext.Site, s => s.Id); e.WebClientContext.Load(itemCollection); e.WebClientContext.ExecuteQueryRetry(); string siteOwner = string.Empty; int totalSolutions = 0; int assemblySolutions = 0; int activeSolutions = 0; int activeAssemblySolutions = 0; foreach (ListItem item in itemCollection) { // We've found solutions totalSolutions++; bool status = false; if (item["Status"] != null) { activeSolutions++; status = true; } bool hasAssembly = false; foreach (string s in item["MetaInfo"].ToString().Split("\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries)) { if (s.Contains("SolutionHasAssemblies")) { if (s.Contains("1")) { assemblySolutions++; if (status) { activeAssemblySolutions++; } hasAssembly = true; } break; } } if (hasAssembly && string.IsNullOrEmpty(siteOwner)) { // Let's add site owners for the solutions which need our attention List <string> admins = new List <string>(); UserCollection users = e.WebClientContext.Web.SiteUsers; e.WebClientContext.Load(users); e.WebClientContext.ExecuteQueryRetry(); foreach (User u in users) { if (u.IsSiteAdmin) { if (!string.IsNullOrEmpty(u.Email) && u.Email.Contains("@")) { admins.Add(u.Email); } } } if (this.Separator == ";") { siteOwner = string.Join(",", admins.ToArray()); } else { siteOwner = string.Join(";", admins.ToArray()); } } SBScanResult result = new SBScanResult() { SiteURL = e.Url, SiteOwner = hasAssembly ? siteOwner : "", WSPName = item["FileLeafRef"].ToString(), Author = ((FieldUserValue)item["Author"]).LookupValue.Replace(",", ""), CreatedDate = Convert.ToDateTime(item["Created"]), Activated = status, HasAssemblies = hasAssembly, SolutionHash = item["SolutionHash"].ToString(), SolutionID = item["SolutionId"].ToString(), SiteId = e.WebClientContext.Site.Id.ToString(), }; // Doing more than a simple scan... if (Mode == Mode.scananddownload || Mode == Mode.scanandanalyze) { // Only download the solution when there's an assembly. By default we're only downloading and scanning each unique solution just once if (hasAssembly && (!SBProcessed.ContainsKey(result.SolutionHash) || Duplicates == true)) { // Add this solution hash to the dictionary SBProcessed.TryAdd(result.SolutionHash, ""); // Download the WSP package ClientResult <Stream> data = item.File.OpenBinaryStream(); e.WebClientContext.Load(item.File); e.WebClientContext.ExecuteQueryRetry(); if (data != null) { int position = 1; int bufferSize = 200000; Byte[] readBuffer = new Byte[bufferSize]; string localFilePath = System.IO.Path.Combine(".", this.OutputFolder, e.WebClientContext.Site.Id.ToString()); System.IO.Directory.CreateDirectory(localFilePath); string wspPath = System.IO.Path.Combine(localFilePath, item["FileLeafRef"].ToString()); using (System.IO.Stream stream = System.IO.File.Create(wspPath)) { while (position > 0) { // data.Value holds the Stream position = data.Value.Read(readBuffer, 0, bufferSize); stream.Write(readBuffer, 0, position); readBuffer = new Byte[bufferSize]; } stream.Flush(); } // Analyze the WSP package by cracking it open and looking inside if (Mode == Mode.scanandanalyze) { Analyzer analyzer = new Analyzer(); analyzer.Init(this.Verbose); var res = analyzer.ProcessFileInfo(System.IO.Path.GetFullPath(wspPath)); result.IsEmptyAssembly = (res.Assemblies.Count == 1 && res.Assemblies[0].ReferencedAssemblies.Count <= 1 && res.Assemblies[0].Classes.Count == 0); result.IsInfoPath = res.InfoPathSolution; result.HasWebParts = (res.WebPartsCount > 0) || (res.UserControlsCount > 0) || res.Features.Where(f => f.WebParts.Any()).Count() > 0; result.HasWebTemplate = res.Features.Where(f => f.WebTemplateDetails.Any()).Count() > 0; result.HasFeatureReceivers = res.FeatureReceiversCount > 0 || res.Features.Where(f => f.FeatureReceivers.Any()).Count() > 0; result.HasEventReceivers = res.EventHandlersCount > 0 || res.Features.Where(f => f.EventReceivers.Any()).Count() > 0; result.HasListDefinition = res.ListTemplatesCount > 0 || res.Features.Where(f => f.ListTemplates.Any()).Count() > 0; result.HasWorkflowAction = res.Features.Where(f => f.WorkflowActionDetails.Any()).Count() > 0; if (res.InfoPathSolution) { result.IsEmptyInfoPathAssembly = IsEmptyInfoPathAssembly(res); } // Dump the analysis results var serializer = new XmlSerializer(typeof(SolutionInformation)); using (var writer = new StreamWriter(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(wspPath), (System.IO.Path.GetFileNameWithoutExtension(wspPath) + ".xml")))) { serializer.Serialize(writer, res); } // Create new package without assembly if (result.IsEmptyAssembly.Value) { string tempFolder = null; tempFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString().Replace("-", "")); // unpack analyzer.UnCab(System.IO.Path.GetFullPath(wspPath), tempFolder); // delete all assemblies var filesToDelete = Directory.GetFiles(tempFolder, "*.*", SearchOption.AllDirectories).Where(s => s.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase)); foreach (var file in filesToDelete) { System.IO.File.Delete(file); } // repack (also deletes the temp folder) analyzer.ReCab(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(wspPath), (System.IO.Path.GetFileNameWithoutExtension(wspPath) + "_fixed.wsp")), tempFolder); } } } } } this.SBScanResults.Push(result); } if (totalSolutions > 0) { Console.WriteLine("Site {0} processed. Found {1} solutions in total of which {2} have assemblies and are activated", e.Url, totalSolutions, activeAssemblySolutions); } } catch (Exception ex) { SBScanError error = new SBScanError() { Error = ex.Message, SiteURL = e.Url, }; this.SBScanErrors.Push(error); Console.WriteLine("Error for site {1}: {0}", ex.Message, e.Url); } }
/// <summary> /// The abstract method which should be implemented in overriden classes to /// </summary> /// <param name="sender">The current timer job instnace</param> /// <param name="e">Timer job run event arguments</param> protected abstract void TimerJobRunImpl(object sender, TimerJobRunEventArgs e);
public virtual void Preprocess(SiteInformation siteCollection, WebInformation web, TimerJobRunEventArgs e) { }
void SimpleJob_TimerJobRun(object sender, TimerJobRunEventArgs e) { e.WebClientContext.Load(e.WebClientContext.Web, p => p.Title); e.WebClientContext.ExecuteQueryRetry(); Console.WriteLine("Site {0} has title {1}", e.Url, e.WebClientContext.Web.Title); }
private void ReferenceScanJob_TimerJobRun(object sender, TimerJobRunEventArgs e) { // thread safe increase of the sites counter IncreaseScannedSites(); try { Console.WriteLine("Processing site {0}...", e.Url); #region Basic sample /* * // Set the first site collection done flag + perform telemetry * SetFirstSiteCollectionDone(e.WebClientContext); * * // add your custom scan logic here, ensure the catch errors as we don't want to terminate scanning * e.WebClientContext.Load(e.WebClientContext.Web, p => p.Title); * e.WebClientContext.ExecuteQueryRetry(); * ScanResult result = new ScanResult() * { * SiteColUrl = e.Url, * SiteURL = e.Url, * SiteTitle = e.WebClientContext.Web.Title * }; * * // Store the scan result * if (!ScanResults.TryAdd(e.Url, result)) * { * ScanError error = new ScanError() * { * SiteURL = e.Url, * SiteColUrl = e.Url, * Error = "Could not add scan result for this site" * }; * this.ScanErrors.Push(error); * } */ #endregion #region Search based sample // Set the first site collection done flag + perform telemetry SetFirstSiteCollectionDone(e.WebClientContext); // Need to use search inside this site collection? List <string> propertiesToRetrieve = new List <string> { "Title", "SPSiteUrl", "FileExtension", "OriginalPath" }; var searchResults = this.Search(e.SiteClientContext.Web, $"((fileextension=htm OR fileextension=html) AND contentclass=STS_ListItem_DocumentLibrary AND Path:{e.Url.TrimEnd('/')}/*)", propertiesToRetrieve); foreach (var searchResult in searchResults) { ScanResult result = new ScanResult() { SiteColUrl = e.Url, FileName = searchResult["OriginalPath"] }; // Get web url var webUrlData = Web.GetWebUrlFromPageUrl(e.SiteClientContext, result.FileName); e.SiteClientContext.ExecuteQueryRetry(); result.SiteURL = webUrlData.Value; // Store the scan result, use FileName as unique key in this sample if (!ScanResults.TryAdd(result.FileName, result)) { ScanError error = new ScanError() { SiteURL = e.Url, SiteColUrl = e.Url, Error = "Could not add scan result for this web" }; this.ScanErrors.Push(error); } } #endregion #region Sub site iteration sample /*** * // Set the first site collection done flag + perform telemetry * SetFirstSiteCollectionDone(e.WebClientContext); * * // Manually iterate over the content * IEnumerable<string> expandedSites = GetAllSubSites(e.SiteClientContext.Site); * bool isFirstSiteInList = true; * string siteCollectionUrl = ""; * * foreach (string site in expandedSites) * { * // thread safe increase of the webs counter * IncreaseScannedWebs(); * * // Clone the existing ClientContext for the sub web * using (ClientContext ccWeb = e.SiteClientContext.Clone(site)) * { * Console.WriteLine("Processing site {0}...", site); * * if (isFirstSiteInList) * { * // Perf optimization: do one call per site to load all the needed properties * var spSite = (ccWeb as ClientContext).Site; * ccWeb.Load(spSite, p => p.RootWeb, p => p.Url); * ccWeb.Load(spSite.RootWeb, p => p.Id); * * isFirstSiteInList = false; * } * * // Perf optimization: do one call per web to load all the needed properties * ccWeb.Load(ccWeb.Web, p => p.Id, p => p.Title); * ccWeb.Load(ccWeb.Web, p => p.WebTemplate, p => p.Configuration); * ccWeb.Load(ccWeb.Web, p => p.Lists.Include(li => li.UserCustomActions, li => li.Title, li => li.Hidden, li => li.DefaultViewUrl, li => li.BaseTemplate, li => li.RootFolder, li => li.ListExperienceOptions)); * ccWeb.ExecuteQueryRetry(); * * // Fill site collection url * if (string.IsNullOrEmpty(siteCollectionUrl)) * { * siteCollectionUrl = ccWeb.Site.Url; * } * * // Need to know if this is a sub site? * if (ccWeb.Web.IsSubSite()) * { * // Sub site specific logic * } * * ScanResult result = new ScanResult() * { * SiteColUrl = e.Url, * SiteURL = site, * SiteTitle = ccWeb.Web.Title, * }; * * // Store the scan result * if (!ScanResults.TryAdd(site, result)) * { * ScanError error = new ScanError() * { * SiteURL = site, * SiteColUrl = e.Url, * Error = "Could not add scan result for this web" * }; * this.ScanErrors.Push(error); * } * } * } **/ #endregion } catch (Exception ex) { ScanError error = new ScanError() { Error = ex.Message, SiteColUrl = e.Url, SiteURL = e.Url, Field1 = "put additional info here" }; this.ScanErrors.Push(error); Console.WriteLine("Error for site {1}: {0}", ex.Message, e.Url); } // Output the scanning progress try { TimeSpan ts = DateTime.Now.Subtract(this.StartTime); Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}. Processed {this.ScannedSites} of {this.SitesToScan} site collections ({Math.Round(((float)this.ScannedSites / (float)this.SitesToScan) * 100)}%). Process running for {ts.Days} days, {ts.Hours} hours, {ts.Minutes} minutes and {ts.Seconds} seconds."); } catch (Exception ex) { Console.WriteLine($"Error showing progress: {ex.ToString()}"); } }
private void ValidateExternalUsersTimerJob_TimerJobRun(object sender, TimerJobRunEventArgs e) { Console.WriteLine("Starting job"); var web = e.SiteClientContext.Web; Tenant tenant = new Tenant(e.TenantClientContext); var siteAdmins = e.SiteClientContext.LoadQuery(web.SiteUsers.Include(u => u.Email).Where(u => u.IsSiteAdmin)); e.SiteClientContext.ExecuteQueryRetry(); List <string> adminEmails = new List <string>(); foreach (var siteAdmin in siteAdmins) { adminEmails.Add(siteAdmin.Email); } SiteProperties p = tenant.GetSitePropertiesByUrl(e.SiteClientContext.Url, true); var sharingCapability = p.EnsureProperty(s => s.SharingCapability); if (sharingCapability != Microsoft.Online.SharePoint.TenantManagement.SharingCapabilities.Disabled) { DateTime checkDate = DateTime.Now; var lastCheckDate = e.WebClientContext.Web.GetPropertyBagValueString(PNPCHECKDATEPROPERTYBAGKEY, string.Empty); if (lastCheckDate == string.Empty) { // new site. Temporary set the check date to less than one Month checkDate = checkDate.AddMonths(-2); } else { if (!DateTime.TryParse(lastCheckDate, out checkDate)) { // Something went wrong with trying to parse the date in the propertybag. Do the check anyway. checkDate = checkDate.AddMonths(-2); } } if (checkDate.AddMonths(1) < DateTime.Now) { e.SiteClientContext.Web.EnsureProperty(w => w.Url); e.WebClientContext.Web.EnsureProperty(w => w.Url); EmailProperties mailProps = new EmailProperties(); mailProps.Subject = "Review required: external users with access to your site"; StringBuilder bodyBuilder = new StringBuilder(); bodyBuilder.AppendFormat("<html><head>{0}</head><body style=\"font-family:sans-serif\">", CSSSTYLE); bodyBuilder.AppendFormat("<p>Your site with address {0} has one or more external users registered. Please review the following list and take appropriate action if such access is not wanted anymore for a user.</p>", e.SiteClientContext.Web.Url); bodyBuilder.Append("<table class=\"tg\"><tr><th>Name</th><th>Invited by</th><th>Created</th><th>Invited As</th><th>Accepted As</th></tr>"); var externalusers = e.TenantClientContext.Web.GetExternalUsersForSiteTenant(new Uri(e.WebClientContext.Web.Url)); if (externalusers.Any()) { foreach (var externalUser in externalusers) { bodyBuilder.AppendFormat("<tr><td>{0}</td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>", externalUser.DisplayName, externalUser.InvitedBy, externalUser.WhenCreated, externalUser.InvitedAs, externalUser.AcceptedAs); } bodyBuilder.Append("</table></body></html>"); mailProps.Body = bodyBuilder.ToString(); mailProps.To = adminEmails.ToArray(); Utility.SendEmail(e.SiteClientContext, mailProps); e.SiteClientContext.ExecuteQueryRetry(); } e.WebClientContext.Web.SetPropertyBagValue(PNPCHECKDATEPROPERTYBAGKEY, DateTime.Now.ToString()); } } Console.WriteLine("Ending job"); }
/// <summary> /// Update HasBroadAccess to 1 for both of the site collection and the current web record if any large security group permission assignment was found on the current web /// </summary> /// <param name="dbSiteRecord">The site collection record</param> /// <param name="dbWebRecord">The current web record</param> /// <param name="e">The timer job run event arguments</param> public override void Preprocess(SiteInformation dbSiteRecord, WebInformation dbWebRecord, TimerJobRunEventArgs e) { var tenant = new Tenant(e.TenantClientContext); var site = tenant.GetSiteByUrl(e.Url); var web = e.Url == dbWebRecord.SiteUrl ? site.RootWeb : site.OpenWeb(e.Url.Substring(dbWebRecord.Name.IndexOf('/') + 1)); // load additional properties could be used to optimize the permission checking process e.TenantClientContext.Load(web, w => w.HasUniqueRoleAssignments, w => w.Url, w => w.ServerRelativeUrl, w => w.ParentWeb.ServerRelativeUrl); var assignments = new List <PermissionAssignment>(); var entries = from groupLoginName in BroadAccessGroups.Keys select new PermissionAssignment { Url = e.Url, Group = groupLoginName, Permission = web.GetUserEffectivePermissions(groupLoginName) }; assignments.AddRange(entries); e.TenantClientContext.ExecuteQuery(); var incompliantAssignments = assignments.Where( p => p.Permission.Value != null && ( p.Permission.Value.Has(PermissionKind.ViewPages) || p.Permission.Value.Has(PermissionKind.ViewListItems))); dbWebRecord.HasBroadAccess = incompliantAssignments.Any(); if (dbWebRecord.HasBroadAccess) { dbWebRecord.BroadAccessGroups = string.Join(";", (from a in incompliantAssignments select a.Group).ToArray()); dbSiteRecord.HasBroadAccess = true; } }
/// <summary> /// Event handler that's being executed by the threads processing the sites. Everything in here must be coded in a thread-safe manner /// </summary> private void AccessAppScanner_TimerJobRun(object sender, TimerJobRunEventArgs e) { try { IEnumerable <string> expandedSites = null; if (!this.UseSearchQuery) { // Get all the sub sites in the site we're processing expandedSites = GetAllSubSites(e.SiteClientContext.Site); } else { expandedSites = new List <string>(); (expandedSites as List <string>).Add(e.Url); } bool isFirstSiteInList = true; string siteCollectionUrl = ""; int viewsRecent = -1; int viewsRecentUnique = -1; int viewsLifetime = -1; int viewsLifetimeUnique = -1; lock (scannedSitesLock) { ScannedSites++; } // Manually iterate over the content foreach (string site in expandedSites) { lock (scannedWebsLock) { ScannedWebs++; } // Create a client context using a AuthenticationManager per domain (without this you'll get access denied on the app domains) using (ClientContext ccWeb = this.CreateClientContext(site)) { ClientResult <ResultTableCollection> siteQueryResults = null; Console.WriteLine("Processing site {0}...", site); try { if (!firstSiteCollectionDone) { firstSiteCollectionDone = true; // Telemetry ccWeb.ClientTag = "SPDev:AccessAppScanner"; ccWeb.Load(ccWeb.Web, p => p.Description, p => p.Id); ccWeb.ExecuteQuery(); } if (isFirstSiteInList) { // Perf optimization: do one call per site to load all the needed properties var spSite = (ccWeb as ClientContext).Site; ccWeb.Load(spSite, p => p.RootWeb, p => p.Url); // Needed in IsSubSite ccWeb.Load(spSite.RootWeb, p => p.Id); // Needed in IsSubSite isFirstSiteInList = false; } // Perf optimization: do one call per web to load all the needed properties ccWeb.Load(ccWeb.Web, p => p.Id, p => p.WebTemplate, p => p.Configuration, p => p.Title, p => p.Created, p => p.AppInstanceId, p => p.ParentWeb, p => p.LastItemUserModifiedDate, p => p.SiteUsers, p => p.AssociatedOwnerGroup); ccWeb.Load(ccWeb.Web.AssociatedOwnerGroup, p => p.Users); ccWeb.ExecuteQueryRetry(); // Fill site collection url if (string.IsNullOrEmpty(siteCollectionUrl)) { siteCollectionUrl = ccWeb.Site.Url; } // Scanning to identify sites that can't show modern pages if (Modes.Contains(Mode.Scan)) { if (ccWeb.Web.WebTemplate.Equals("ACCSVC", StringComparison.InvariantCultureIgnoreCase) || ccWeb.Web.WebTemplate.Equals("ACCSRV", StringComparison.InvariantCultureIgnoreCase)) { // Get the date when this Access App was last accessed var lastAccessedDate = ccWeb.Web.GetPropertyBagValueString("accsvcLastAccessedDate", ""); var lastModifiedDate = ccWeb.Web.LastItemUserModifiedDate; // Query for usage on the actual site collection, do this only once per site collection if (viewsRecent == -1) { KeywordQuery siteQuery = new KeywordQuery(e.SiteClientContext); siteQuery.QueryText = string.Format("path:{0} NOT(contentclass:STS_Site) NOT(contentclass:STS_Web)", siteCollectionUrl); siteQuery.SelectProperties.Clear(); siteQuery.SelectProperties.Add("ViewsRecent"); siteQuery.SelectProperties.Add("ViewsRecentUniqueUsers"); siteQuery.SelectProperties.Add("ViewsLifeTime"); siteQuery.SelectProperties.Add("ViewsLifeTimeUniqueUsers"); siteQuery.SortList.Add("ViewsRecent", SortDirection.Descending); siteQuery.TrimDuplicates = false; siteQuery.RowLimit = 500; SearchExecutor seachExecutor = new SearchExecutor(e.SiteClientContext); siteQueryResults = seachExecutor.ExecuteQuery(siteQuery); // Load site collection site users e.SiteClientContext.Load(e.SiteClientContext.Web, p => p.SiteUsers); e.SiteClientContext.ExecuteQueryRetry(); // Fill site usage information if (siteQueryResults != null) { viewsRecent = 0; viewsRecentUnique = 0; viewsLifetime = 0; viewsLifetimeUnique = 0; if (siteQueryResults.Value != null) { foreach (ResultTable t in siteQueryResults.Value) { foreach (IDictionary <string, object> r in t.ResultRows) { viewsRecent += r["ViewsRecent"] != null?int.Parse(r["ViewsRecent"].ToString()) : 0; viewsRecentUnique += r["ViewsRecentUniqueUsers"] != null?int.Parse(r["ViewsRecentUniqueUsers"].ToString()) : 0; viewsLifetime += r["ViewsLifeTime"] != null?int.Parse(r["ViewsLifeTime"].ToString()) : 0; viewsLifetimeUnique += r["ViewsLifeTimeUniqueUsers"] != null?int.Parse(r["ViewsLifeTimeUniqueUsers"].ToString()) : 0; } } } } } var accessAppResult = new AccessAppScanData() { ViewsRecent = viewsRecent, ViewsRecentUnique = viewsRecentUnique, ViewsLifetime = viewsLifetime, ViewsLifetimeUnique = viewsLifetimeUnique, SiteColUrl = siteCollectionUrl, SiteUrl = site, WebTitle = ccWeb.Web.Title, WebCreatedDate = ccWeb.Web.Created, WebTemplate = $"{ccWeb.Web.WebTemplate}#{ccWeb.Web.Configuration}", AppInstanceId = ccWeb.Web.AppInstanceId, WebId = ccWeb.Web.Id, LastAccessedDate = lastAccessedDate, LastModifiedByUserDate = (ccWeb.Web.WebTemplate.Equals("ACCSRV", StringComparison.InvariantCultureIgnoreCase) ? lastModifiedDate.ToString() : "") }; Console.WriteLine($"Access App found in {site}."); // grab the site collection administrators if (e.SiteClientContext.Web.SiteUsers != null) { try { var admins = e.SiteClientContext.Web.SiteUsers.Where(p => p.IsSiteAdmin); if (admins != null && admins.Count() > 0) { foreach (var admin in admins) { if (!string.IsNullOrEmpty(admin.Email)) { accessAppResult.SiteAdmins = AddSiteOwner(accessAppResult.SiteAdmins, admin.Email); } } } } catch { //Eat exceptions...rather log all Access Apps i the main result list instead of dropping some due to error getting owners } } try { // grab folks from the Access Web App site owners group if (ccWeb.Web.AssociatedOwnerGroup != null && ccWeb.Web.AssociatedOwnerGroup.Users != null && ccWeb.Web.AssociatedOwnerGroup.Users.Count > 0) { foreach (var owner in ccWeb.Web.AssociatedOwnerGroup.Users) { if (!string.IsNullOrEmpty(owner.Email)) { accessAppResult.SiteAdmins = AddSiteOwner(accessAppResult.SiteAdmins, owner.Email); } } } } catch { //Eat exceptions...rather log all Access Apps i the main result list instead of dropping some due to error getting owners } if (!string.IsNullOrEmpty(ccWeb.Web.ParentWeb.ServerRelativeUrl)) { Uri siteCollectionUri = new Uri(siteCollectionUrl); string parentWebUrl = $"{siteCollectionUri.Scheme}://{siteCollectionUri.DnsSafeHost}:{siteCollectionUri.Port}{ccWeb.Web.ParentWeb.ServerRelativeUrl}"; accessAppResult.ParentSiteUrl = parentWebUrl; using (var ccParent = this.CreateClientContext(parentWebUrl)) { try { // Get users from parent web site owners group ccParent.Load(ccParent.Web, p => p.AssociatedOwnerGroup); ccParent.Load(ccParent.Web.AssociatedOwnerGroup, p => p.Users); // Check app status AppInstance appInstance = null; if (accessAppResult.AppInstanceId != Guid.Empty) { appInstance = ccParent.Web.GetAppInstanceById(accessAppResult.AppInstanceId); ccParent.Load(appInstance); } ccParent.ExecuteQueryRetry(); if (accessAppResult.AppInstanceId != Guid.Empty) { accessAppResult.AppInstanceStatus = appInstance.Status.ToString(); } try { // Process parent web owners if (ccParent.Web.AssociatedOwnerGroup != null && ccParent.Web.AssociatedOwnerGroup.Users != null && ccParent.Web.AssociatedOwnerGroup.Users.Count > 0) { foreach (var owner in ccParent.Web.AssociatedOwnerGroup.Users) { if (!string.IsNullOrEmpty(owner.Email)) { accessAppResult.SiteAdmins = AddSiteOwner(accessAppResult.SiteAdmins, owner.Email); } } } } catch { //Eat exceptions...rather log all Access Apps i the main result list instead of dropping some due to error getting owners } } catch (Exception ex) { AccessAppScanError error = new AccessAppScanError() { Error = ex.Message, SiteUrl = site, SiteColUrl = siteCollectionUrl }; this.AccessAppScanErrors.Push(error); Console.WriteLine("Error while reading Access App status for {1}: {0}", ex.Message, site); } } } AccessAppResults.Push(accessAppResult); } } } catch (Exception ex) { AccessAppScanError error = new AccessAppScanError() { Error = ex.ToDetailedString(), SiteUrl = site, SiteColUrl = siteCollectionUrl }; this.AccessAppScanErrors.Push(error); Console.WriteLine("Error for site {1}: {0}", ex.Message, site); } } try { TimeSpan ts = DateTime.Now.Subtract(this.StartTime); Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}. Processed {this.ScannedSites} of {this.SitesToScan} site collections ({Math.Round(((float)this.ScannedSites / (float)this.SitesToScan) * 100)}%). Process running for {ts.Days} days, {ts.Hours} hours, {ts.Minutes} minutes and {ts.Seconds} seconds."); } catch (Exception ex) { Console.WriteLine($"Error showing progress: {ex.ToDetailedString()}"); } } } catch (Exception ex) { AccessAppScanError error = new AccessAppScanError() { Error = ex.ToDetailedString(), SiteUrl = e.SiteClientContext.Site.Url, SiteColUrl = e.SiteClientContext.Site.Url }; this.AccessAppScanErrors.Push(error); Console.WriteLine("Error for site {1}: {0}", ex.Message, e.SiteClientContext.Site.Url); } }
/// <summary> /// Event handler that's being executed by the threads processing the sites. Everything in here must be coded in a thread-safe manner /// </summary> private void UIExperienceScanner_TimerJobRun(object sender, TimerJobRunEventArgs e) { // Get all the sub sites in the site we're processing IEnumerable <string> expandedSites = GetAllSubSites(e.SiteClientContext.Site); bool isFirstSiteInList = true; string siteCollectionUrl = ""; lock (scannedSitesLock) { ScannedSites++; } // Manually iterate over the content foreach (string site in expandedSites) { lock (scannedWebsLock) { ScannedWebs++; } // Clone the existing ClientContext for the sub web using (ClientContext ccWeb = e.SiteClientContext.Clone(site)) { Console.WriteLine("Processing site {0}...", site); try { if (!firstSiteCollectionDone) { firstSiteCollectionDone = true; // Telemetry ccWeb.ClientTag = "SPDev:UIExperienceScanner"; ccWeb.Load(ccWeb.Web, p => p.Description, p => p.Id); ccWeb.ExecuteQuery(); } if (isFirstSiteInList) { // Perf optimization: do one call per site to load all the needed properties var spSite = (ccWeb as ClientContext).Site; ccWeb.Load(spSite, p => p.RootWeb, p => p.Url); // Needed in IsSubSite ccWeb.Load(spSite.RootWeb, p => p.Id); // Needed in IsSubSite if (Modes.Contains(Mode.Scan) || Modes.Contains(Mode.IgnoredCustomizations)) { ccWeb.Load(spSite, p => p.UserCustomActions); // User custom action site level } if (Modes.Contains(Mode.Scan) || Modes.Contains(Mode.BlockedLists)) { ccWeb.Load(spSite, p => p.Features); // Features site level } isFirstSiteInList = false; } // Perf optimization: do one call per web to load all the needed properties ccWeb.Load(ccWeb.Web, p => p.Id); // Needed in IsSubSite if (Modes.Contains(Mode.Scan) || Modes.Contains(Mode.IgnoredCustomizations)) { ccWeb.Load(ccWeb.Web, p => p.MasterUrl, p => p.CustomMasterUrl, // master page check p => p.AlternateCssUrl, // Alternate CSS p => p.UserCustomActions); // Web user custom actions } if (Modes.Contains(Mode.Scan) || Modes.Contains(Mode.BlockedPages) || Modes.Contains(Mode.BlockedLists)) { ccWeb.Load(ccWeb.Web, p => p.Features); // Features web level } // WebTemplate and Lists is needed in all three scenarios, so we always prefetch them. ccWeb.Load(ccWeb.Web, p => p.WebTemplate, p => p.Configuration); ccWeb.Load(ccWeb.Web, p => p.Lists.Include(li => li.UserCustomActions, li => li.Title, li => li.Hidden, li => li.DefaultViewUrl, li => li.BaseTemplate, li => li.RootFolder, li => li.ListExperienceOptions)); // List check, includes list user custom actions ccWeb.ExecuteQueryRetry(); // Fill site collection url if (string.IsNullOrEmpty(siteCollectionUrl)) { siteCollectionUrl = ccWeb.Site.Url; } // Scanning to identify sites that can't show modern pages if (Modes.Contains(Mode.Scan) || Modes.Contains(Mode.BlockedPages)) { PageScanner featureScanner = new PageScanner(site, siteCollectionUrl); var featureScanResult = featureScanner.Analyze(ccWeb); if (featureScanResult != null) { if (!this.PageResults.TryAdd(Guid.NewGuid().ToString() + featureScanResult.Url, featureScanResult)) { UIExperienceScanError error = new UIExperienceScanError() { Error = $"Could not add page scan result for {featureScanResult.Url}", SiteColUrl = siteCollectionUrl, SiteURL = site, }; this.UIExpScanErrors.Push(error); Console.WriteLine($"Could not add page scan result for {featureScanResult.Url}"); } } } // Scanning to identify lists which can't be shown in modern if (Modes.Contains(Mode.Scan) || Modes.Contains(Mode.BlockedLists)) { ListScanner listConfig = new ListScanner(site, siteCollectionUrl, this.ExcludeListsOnlyBlockedByOobReasons); var scannedLists = listConfig.Analyze(ccWeb, ref this.ListResults, ref this.UIExpScanErrors); lock (scannedListsLock) { this.ScannedLists = this.ScannedLists + scannedLists; } } // Scanning for customizations which will be ignored in modern pages and lists if (Modes.Contains(Mode.Scan) || Modes.Contains(Mode.IgnoredCustomizations)) { // Custom CSS scanner AlternateCSSScanner cssScanner = new AlternateCSSScanner(site, siteCollectionUrl); var alternateCSSResult = cssScanner.Analyze(ccWeb); if (alternateCSSResult != null) { this.AlternateCSSResults.Push(alternateCSSResult); if (this.CustomizationResults.ContainsKey(alternateCSSResult.Url)) { var customizationResult = this.CustomizationResults[alternateCSSResult.Url]; customizationResult.IgnoredAlternateCSS = alternateCSSResult.AlternateCSS; if (!this.CustomizationResults.TryUpdate(alternateCSSResult.Url, customizationResult, customizationResult)) { UIExperienceScanError error = new UIExperienceScanError() { Error = $"Could not update CSS scan result for {customizationResult.Url}", SiteURL = site, SiteColUrl = siteCollectionUrl }; this.UIExpScanErrors.Push(error); Console.WriteLine($"Could not update CSS scan result for {customizationResult.Url}"); } } else { var customizationResult = new CustomizationResult() { SiteUrl = alternateCSSResult.SiteUrl, Url = alternateCSSResult.Url, SiteColUrl = alternateCSSResult.SiteColUrl, IgnoredAlternateCSS = alternateCSSResult.AlternateCSS }; if (!this.CustomizationResults.TryAdd(customizationResult.Url, customizationResult)) { UIExperienceScanError error = new UIExperienceScanError() { Error = $"Could not add CSS scan result for {customizationResult.Url}", SiteURL = site, SiteColUrl = siteCollectionUrl }; this.UIExpScanErrors.Push(error); Console.WriteLine($"Could not add CSS scan result for {customizationResult.Url}"); } } } // Custom master page scanner MasterPageScanner masterScanner = new MasterPageScanner(site, siteCollectionUrl); var masterPageResult = masterScanner.Analyze(ccWeb); if (masterPageResult != null) { this.MasterPageResults.Push(masterPageResult); if (this.CustomizationResults.ContainsKey(masterPageResult.Url)) { var customizationResult = this.CustomizationResults[masterPageResult.Url]; customizationResult.IgnoredMasterPage = masterPageResult.MasterPage; customizationResult.IgnoredCustomMasterPage = masterPageResult.CustomMasterPage; if (!this.CustomizationResults.TryUpdate(masterPageResult.Url, customizationResult, customizationResult)) { UIExperienceScanError error = new UIExperienceScanError() { Error = $"Could not update MasterPage scan result for {customizationResult.Url}", SiteURL = site, SiteColUrl = siteCollectionUrl }; this.UIExpScanErrors.Push(error); Console.WriteLine($"Could not update MasterPage scan result for {customizationResult.Url}"); } } else { var customizationResult = new CustomizationResult() { SiteUrl = masterPageResult.SiteUrl, Url = masterPageResult.Url, SiteColUrl = masterPageResult.SiteColUrl, IgnoredMasterPage = masterPageResult.MasterPage, IgnoredCustomMasterPage = masterPageResult.CustomMasterPage }; if (!this.CustomizationResults.TryAdd(customizationResult.Url, customizationResult)) { UIExperienceScanError error = new UIExperienceScanError() { Error = $"Could not add MasterPage scan result for {customizationResult.Url}", SiteURL = site, SiteColUrl = siteCollectionUrl }; this.UIExpScanErrors.Push(error); Console.WriteLine($"Could not add MasterPage scan result for {customizationResult.Url}"); } } } // Custom action scanner CustomActionScanner customActions = new CustomActionScanner(site, siteCollectionUrl); customActions.Analyze(ccWeb, ref this.CustomActionScanResults, ref this.CustomizationResults, ref this.UIExpScanErrors); } } catch (Exception ex) { UIExperienceScanError error = new UIExperienceScanError() { Error = ex.Message, SiteURL = site, SiteColUrl = siteCollectionUrl }; this.UIExpScanErrors.Push(error); Console.WriteLine("Error for site {1}: {0}", ex.Message, site); } } try { TimeSpan ts = DateTime.Now.Subtract(this.StartTime); Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}. Processed {this.ScannedSites} of {this.SitesToScan} site collections ({Math.Round(((float)this.ScannedSites / (float)this.SitesToScan) * 100)}%). Process running for {ts.Days} days, {ts.Hours} hours, {ts.Minutes} minutes and {ts.Seconds} seconds."); } catch (Exception ex) { Console.WriteLine($"Error showing progress: {ex.ToString()}"); } } }