diff --git a/SharepointToolbox/AssemblyInfo.cs b/SharepointToolbox/AssemblyInfo.cs new file mode 100644 index 0000000..aa97970 --- /dev/null +++ b/SharepointToolbox/AssemblyInfo.cs @@ -0,0 +1,13 @@ +using System.Runtime.CompilerServices; +using System.Windows; + +[assembly: InternalsVisibleTo("SharepointToolbox.Tests")] + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/SharepointToolbox/Core/Models/SiteInfo.cs b/SharepointToolbox/Core/Models/SiteInfo.cs new file mode 100644 index 0000000..9408e09 --- /dev/null +++ b/SharepointToolbox/Core/Models/SiteInfo.cs @@ -0,0 +1,3 @@ +namespace SharepointToolbox.Core.Models; + +public record SiteInfo(string Url, string Title); diff --git a/SharepointToolbox/Services/ISiteListService.cs b/SharepointToolbox/Services/ISiteListService.cs new file mode 100644 index 0000000..9086be9 --- /dev/null +++ b/SharepointToolbox/Services/ISiteListService.cs @@ -0,0 +1,11 @@ +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Services; + +public interface ISiteListService +{ + Task> GetSitesAsync( + TenantProfile profile, + IProgress progress, + CancellationToken ct); +} diff --git a/SharepointToolbox/Services/SiteListService.cs b/SharepointToolbox/Services/SiteListService.cs new file mode 100644 index 0000000..2311e97 --- /dev/null +++ b/SharepointToolbox/Services/SiteListService.cs @@ -0,0 +1,69 @@ +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...")); + + var adminUrl = DeriveAdminUrl(profile.TenantUrl); + var adminProfile = new TenantProfile + { + Name = profile.Name, + TenantUrl = adminUrl, + ClientId = profile.ClientId + }; + + ClientContext adminCtx; + try + { + adminCtx = await _sessionManager.GetOrCreateContextAsync(adminProfile, 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 tenant = new Tenant(adminCtx); + var siteProps = tenant.GetSitePropertiesFromSharePoint("", true); + adminCtx.Load(siteProps); + await adminCtx.ExecuteQueryAsync(); + + ct.ThrowIfCancellationRequested(); + + return siteProps + .Where(s => s.Status == "Active" + && !s.Url.Contains("-my.sharepoint.com", StringComparison.OrdinalIgnoreCase)) + .Select(s => new SiteInfo(s.Url, s.Title)) + .OrderBy(s => s.Url) + .ToList(); + } + + internal static string DeriveAdminUrl(string tenantUrl) + => Regex.Replace(tenantUrl.TrimEnd('/'), + @"(https://[^.]+)(\.sharepoint\.com)", + "$1-admin$2", + RegexOptions.IgnoreCase); +}