/// <summary> /// Processes the amount of work that will be done for a single site/web /// </summary> /// <param name="site">Url of the site to process</param> private void DoWork(string site) { // Get the root site of the passed site string rootSite = GetRootSite(site); // Instantiate the needed ClientContext objects ClientContext ccWeb = CreateClientContext(site); ClientContext ccSite = null; if (rootSite.Equals(site, StringComparison.InvariantCultureIgnoreCase)) { ccSite = ccWeb; } else { ccSite = CreateClientContext(rootSite); } #if !ONPREMISES // Instantiate ClientContext against tenant admin site, this is needed to operate using the Tenant API string tenantAdminSiteUrl = tenantAdminSite; if (string.IsNullOrEmpty(tenantAdminSiteUrl)) { tenantAdminSiteUrl = GetTenantAdminSite(site); } ClientContext ccTenant = CreateClientContext(tenantAdminSiteUrl); #else // No easy way to detect tenant admin site in on-premises, so uses has to specify it ClientContext ccTenant = null; if (!String.IsNullOrEmpty(tenantAdminSite)) { ccTenant = CreateClientContext(tenantAdminSite); } #endif // Prepare the timerjob callback event arguments TimerJobRunEventArgs e = new TimerJobRunEventArgs(site, ccSite, ccWeb, ccTenant, null, null, "", new Dictionary<string, string>(), this.ConfigurationData); // Trigger the event to fire, but only when there's an event handler connected if (TimerJobRun != null) { OnTimerJobRun(e); } }
/// <summary> /// Triggers the event to fire and deals with all the pre/post processing needed to automatically manage state /// </summary> /// <param name="e">TimerJobRunEventArgs event arguments class that will be passed to the event handler</param> private void OnTimerJobRun(TimerJobRunEventArgs e) { try { // Copy for thread safety? TimerJobRunHandler timerJobRunHandlerThreadCopy = TimerJobRun; if (timerJobRunHandlerThreadCopy != null) { PropertyValues props = null; JavaScriptSerializer s = null; // if state is managed then the state value is stored in a property named "<timerjobname>_Properties" string propertyKey = String.Format("{0}_Properties", NormalizedTimerJobName(this.name)); // read the properties from the web property bag if (this.manageState) { props = e.WebClientContext.Web.AllProperties; e.WebClientContext.Load(props); e.WebClientContext.ExecuteQueryRetry(); s = new JavaScriptSerializer(); // we've found previously stored state, so this is not the first timer job run if (props.FieldValues.ContainsKey(propertyKey)) { string timerJobProps = props.FieldValues[propertyKey].ToString(); // We should have a value, but you never know... if (!string.IsNullOrEmpty(timerJobProps)) { // Deserialize the json string into a TimerJobRun class instance TimerJobRun timerJobRunProperties = s.Deserialize<TimerJobRun>(timerJobProps); // Pass the state information as part of the event arguments if (timerJobRunProperties != null) { e.PreviousRun = timerJobRunProperties.PreviousRun; e.PreviousRunSuccessful = timerJobRunProperties.PreviousRunSuccessful; e.PreviousRunVersion = timerJobRunProperties.PreviousRunVersion; e.Properties = timerJobRunProperties.Properties; } } } } // trigger the event timerJobRunHandlerThreadCopy(this, e); // Update and store the properties to the web property bag if (this.manageState) { // Retrieve the values of the event arguments and complete them with defaults TimerJobRun timerJobRunProperties = new TimerJobRun() { PreviousRun = DateTime.Now, PreviousRunSuccessful = e.CurrentRunSuccessful, PreviousRunVersion = this.version, Properties = e.Properties, }; // Serialize to json string string timerJobProps = s.Serialize(timerJobRunProperties); props = e.WebClientContext.Web.AllProperties; // Get the value, if the web properties are already loaded if (props.FieldValues.Count > 0) { props[propertyKey] = timerJobProps; } else { // Load the web properties e.WebClientContext.Load(props); e.WebClientContext.ExecuteQueryRetry(); props[propertyKey] = timerJobProps; } // Persist the web property bag entries e.WebClientContext.Web.Update(); e.WebClientContext.ExecuteQueryRetry(); } } } catch (Exception ex) { // Catch error in this case as we don't want to the whole program to terminate if one single site operation fails } }
/// <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"] != null ? item["SolutionHash"].ToString() : "", SolutionID = item["SolutionId"] != null ? 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); } }