d69c3290d8
Reviewed-on: #2
77 lines
3.2 KiB
C#
77 lines
3.2 KiB
C#
using Microsoft.Graph;
|
|
using Microsoft.Graph.Models;
|
|
using Microsoft.Kiota.Abstractions;
|
|
using SharepointToolbox.Web.Core.Models;
|
|
using AppGraphClientFactory = SharepointToolbox.Web.Infrastructure.Auth.GraphClientFactory;
|
|
|
|
namespace SharepointToolbox.Web.Services;
|
|
|
|
/// <summary>
|
|
/// Delegated Graph implementation of <see cref="ISiteDiscoveryService"/>.
|
|
/// Uses the <c>/sites?search=*</c> endpoint, paging through every result.
|
|
/// Requires the delegated <c>Sites.Read.All</c> scope.
|
|
/// </summary>
|
|
public class SiteDiscoveryService : ISiteDiscoveryService
|
|
{
|
|
private readonly AppGraphClientFactory _graphClientFactory;
|
|
|
|
public SiteDiscoveryService(AppGraphClientFactory graphClientFactory)
|
|
{
|
|
_graphClientFactory = graphClientFactory;
|
|
}
|
|
|
|
public async Task<IReadOnlyList<SiteInfo>> SearchSitesAsync(
|
|
TenantProfile profile,
|
|
string? query = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var graphClient = await _graphClientFactory.CreateClientAsync(profile);
|
|
// "*" is the Graph convention for "return all sites".
|
|
var search = string.IsNullOrWhiteSpace(query) ? "*" : query!;
|
|
|
|
// The typed Sites.GetAsync maps its Search property to OData "$search",
|
|
// which routes "*" through KQL and fails ("'*' is not valid at position 0").
|
|
// The all-sites wildcard only works via the bare, non-OData "search"
|
|
// query parameter, so build the request manually.
|
|
var requestInfo = new RequestInformation
|
|
{
|
|
HttpMethod = Method.GET,
|
|
UrlTemplate = "{+baseurl}/sites{?search,%24top}",
|
|
PathParameters = new Dictionary<string, object>
|
|
{
|
|
{ "baseurl", graphClient.RequestAdapter.BaseUrl ?? "https://graph.microsoft.com/v1.0" }
|
|
},
|
|
};
|
|
requestInfo.QueryParameters.Add("search", search);
|
|
requestInfo.QueryParameters.Add("%24top", 999);
|
|
requestInfo.Headers.Add("Accept", "application/json");
|
|
|
|
var response = await graphClient.RequestAdapter.SendAsync<SiteCollectionResponse>(
|
|
requestInfo, SiteCollectionResponse.CreateFromDiscriminatorValue, cancellationToken: ct);
|
|
|
|
if (response is null) return Array.Empty<SiteInfo>();
|
|
|
|
var results = new List<SiteInfo>();
|
|
var iter = PageIterator<Site, SiteCollectionResponse>.CreatePageIterator(
|
|
graphClient, response,
|
|
site =>
|
|
{
|
|
if (ct.IsCancellationRequested) return false;
|
|
var url = site.WebUrl ?? string.Empty;
|
|
if (string.IsNullOrEmpty(url)) return true;
|
|
// Skip OneDrive personal sites — not useful for these scans.
|
|
if (url.Contains("/personal/", StringComparison.OrdinalIgnoreCase)) return true;
|
|
var title = site.DisplayName ?? site.Name ?? url;
|
|
results.Add(new SiteInfo(url, title));
|
|
return true;
|
|
});
|
|
await iter.IterateAsync(ct);
|
|
|
|
return results
|
|
.GroupBy(s => s.Url, StringComparer.OrdinalIgnoreCase)
|
|
.Select(g => g.First())
|
|
.OrderBy(s => s.Title, StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
}
|
|
}
|