using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Online.SharePoint.TenantAdministration; using Microsoft.SharePoint.Client; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Services; public class SiteListService : ISiteListService { private readonly SessionManager _sessionManager; public SiteListService(SessionManager sessionManager) { _sessionManager = sessionManager; } public async Task> GetSitesAsync( TenantProfile profile, IProgress progress, CancellationToken ct) { ct.ThrowIfCancellationRequested(); progress.Report(OperationProgress.Indeterminate("Loading sites...")); // Obtain the already-authenticated context for the tenant URL, then clone it to // the admin URL. Cloning reuses the existing token — no second interactive login. ClientContext ctx; try { ctx = await _sessionManager.GetOrCreateContextAsync(profile, ct); } catch (ServerException ex) when (ex.Message.Contains("Access denied", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException( "Site listing requires SharePoint administrator permissions. Connect with an admin account.", ex); } var adminUrl = DeriveAdminUrl(profile.TenantUrl); using var adminCtx = ctx.Clone(adminUrl); var results = new List(); var tenant = new Tenant(adminCtx); SPOSitePropertiesEnumerable? batch = null; // Paginate through all site collections using GetSitePropertiesFromSharePointByFilters. // StartIndex = null on the first call; subsequent calls use NextStartIndexFromSharePoint. while (batch == null || batch.NextStartIndexFromSharePoint != null) { ct.ThrowIfCancellationRequested(); var filter = new SPOSitePropertiesEnumerableFilter { IncludePersonalSite = PersonalSiteFilter.UseServerDefault, StartIndex = batch?.NextStartIndexFromSharePoint, IncludeDetail = true }; batch = tenant.GetSitePropertiesFromSharePointByFilters(filter); adminCtx.Load(batch); await adminCtx.ExecuteQueryAsync(); foreach (var s in batch) { if (s.Status == "Active" && !s.Url.Contains("-my.sharepoint.com", StringComparison.OrdinalIgnoreCase)) { results.Add(new SiteInfo(s.Url, s.Title)); } } } return results.OrderBy(s => s.Url).ToList(); } internal static string DeriveAdminUrl(string tenantUrl) => Regex.Replace(tenantUrl.TrimEnd('/'), @"(https://[^.]+)(\.sharepoint\.com)", "$1-admin$2", RegexOptions.IgnoreCase); }