From 3137ea09888d4c848239c5f89edefb3feec1fb67 Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Fri, 10 Nov 2023 16:39:42 +0100 Subject: [PATCH] Adding `-Detailed` option to `Get-PnPTenantDeletedSite` (#3550) * Adding -Detailed option to Get-PnPTenantDeletedSite to optionally fetch more information on the deleted sites * Adding changelog entry * Adding PR reference --------- Co-authored-by: Gautam Sheth --- CHANGELOG.md | 1 + documentation/Get-PnPTenantDeletedSite.md | 62 ++++-- src/Commands/Admin/GetTenantDeletedSite.cs | 17 +- src/Commands/Model/SPODeletedSite.cs | 214 ++++++++++++++++++--- 4 files changed, 238 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc0ae5e2..b52236d4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added `RestrictedAccessControl`, `ClearRestrictedAccessControl`, `RemoveRestrictedAccessControlGroups`, `AddRestrictedAccessControlGroups` and `RestrictedAccessControlGroups` parameters to `Set-PnPTenantSite` cmdlet to handle restricted access control. [#3463](https://github.com/pnp/powershell/pull/3463) - Added `Get-PnPRetentionLabel` cmdlet to retrieve Purview retention labels. [#3459](https://github.com/pnp/powershell/pull/3459) - Added GCC support for `Get-PnPAzureADUser` , `Add-PnPFlowOwner` , `Remove-PnPFlowOwner`, `Sync-PnPSharePointUserProfilesFromAzureActiveDirectory`, `New-PnPAzureADUserTemporaryAccessPass` and `Get-PnPAvailableSensitivityLabel` cmdlets. [#3484](https://github.com/pnp/powershell/pull/3484) +- Added `-Detailed` option to `Get-PnPTenantDeletedSite` to optionally fetch more information on the deleted sites [#3550](https://github.com/pnp/powershell/pull/3550) - Added a devcontainer for easily building a minimal environment necessary to contribute to the project. [#3497](https://github.com/pnp/powershell/pull/3497) - Added `-RelativeUrl` parameter to `Connect-PnPOnline` cmdlet to allow specifying custom URLs for usage with `-WebLogin` method. [#3530](https://github.com/pnp/powershell/pull/3530) - Added `-RetryCount` to `Submit-PnPSearchQuery` which allows for specifying the number of retries to perform when an exception occurs [#3528](https://github.com/pnp/powershell/pull/3528) diff --git a/documentation/Get-PnPTenantDeletedSite.md b/documentation/Get-PnPTenantDeletedSite.md index 6f8f1c0a6..ee25fb9f1 100644 --- a/documentation/Get-PnPTenantDeletedSite.md +++ b/documentation/Get-PnPTenantDeletedSite.md @@ -20,11 +20,11 @@ Fetches the site collections from the tenant recycle bin. ## SYNTAX ```powershell -Get-PnPTenantDeletedSite [-Identity] [-Limit] [-IncludePersonalSite] [-IncludeOnlyPersonalSite] [-Connection ] +Get-PnPTenantDeletedSite [-Identity] [-Limit] [-IncludePersonalSite] [-IncludeOnlyPersonalSite] [-Detailed] [-Verbose] [-Connection ] ``` ## DESCRIPTION -Fetches the site collection's which are listed in your tenant's recycle bin. +Fetches the site collections which are listed in your tenant's recycle bin. ## EXAMPLES @@ -33,23 +33,30 @@ Fetches the site collection's which are listed in your tenant's recycle bin. Get-PnPTenantDeletedSite ``` -This will fetch the site collections from the recycle bin. +This will fetch basic information on site collections located in the recycle bin. ### EXAMPLE 2 ```powershell -Get-PnPTenantDeletedSite -Identity "https://tenant.sharepoint.com/sites/contoso" +Get-PnPTenantDeletedSite -Detailed ``` -This will fetch the site collection with the url 'https://tenant.sharepoint.com/sites/contoso' from the recycle bin and display its properties. +This will fetch detailed information on site collections located in the recycle bin. ### EXAMPLE 3 ```powershell +Get-PnPTenantDeletedSite -Identity "https://tenant.sharepoint.com/sites/contoso" +``` + +This will fetch basic information on the site collection with the url 'https://tenant.sharepoint.com/sites/contoso' from the recycle bin. + +### EXAMPLE 4 +```powershell Get-PnPTenantDeletedSite -IncludePersonalSite ``` This will fetch the site collections from the recycle bin including the personal sites and display its properties. -### EXAMPLE 4 +### EXAMPLE 5 ```powershell Get-PnPTenantDeletedSite -IncludeOnlyPersonalSite ``` @@ -72,8 +79,8 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -Limit -Limit of the number of site collections to be retrieved from the recycle bin. Default is 200. +### -Detailed +When specified, detailed information will be returned on the site collections. This will take longer to execute. ```yaml Type: SwitchParameter @@ -86,12 +93,26 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -IncludePersonalSite -If specified the task will also retrieve the personal sites from the recycle bin. +### -Identity +Specifies the full URL of the site collection that needs to be restored. + +```yaml +Type: String +Parameter Sets: (All) + +Required: False +Position: 0 +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -IncludeOnlyPersonalSite +If specified the task will only retrieve the personal sites from the recycle bin. ```yaml Type: SwitchParameter -Parameter Sets: (All, ParameterSetAllSites) +Parameter Sets: (ParameterSetPersonalSitesOnly) Required: False Position: Named @@ -100,12 +121,12 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -IncludeOnlyPersonalSite -If specified the task will only retrieve the personal sites from the recycle bin. +### -IncludePersonalSite +If specified the task will also retrieve the personal sites from the recycle bin. ```yaml Type: SwitchParameter -Parameter Sets: (All, ParameterSetPersonalSitesOnly) +Parameter Sets: (ParameterSetAllSites) Required: False Position: Named @@ -114,21 +135,20 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -Identity -Specifies the full URL of the site collection that needs to be restored. +### -Limit +Limit of the number of site collections to be retrieved from the recycle bin. Default is 200. ```yaml -Type: String +Type: SwitchParameter Parameter Sets: (All) Required: False -Position: 0 +Position: Named Default value: None -Accept pipeline input: True (ByValue) +Accept pipeline input: False Accept wildcard characters: False ``` ## RELATED LINKS -[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) \ No newline at end of file diff --git a/src/Commands/Admin/GetTenantDeletedSite.cs b/src/Commands/Admin/GetTenantDeletedSite.cs index c9853713e..6ef8f30ab 100644 --- a/src/Commands/Admin/GetTenantDeletedSite.cs +++ b/src/Commands/Admin/GetTenantDeletedSite.cs @@ -12,6 +12,9 @@ namespace PnP.PowerShell.Commands.Admin [Cmdlet(VerbsCommon.Get, "PnPTenantDeletedSite")] public class GetTenantDeletedSite : PnPAdminCmdlet { + private const string ParameterSet_ALLSITES = "ParameterSetAllSites"; + private const string ParameterSet_PERSONALSITESONLY = "ParameterSetPersonalSitesOnly"; + [Parameter(Position = 0, ValueFromPipeline = true, Mandatory = false)] [Alias("Url")] public SPOSitePipeBind Identity { get; set; } @@ -19,12 +22,16 @@ public class GetTenantDeletedSite : PnPAdminCmdlet [Parameter(Mandatory = false)] public uint Limit = 200; - [Parameter(ParameterSetName = "ParameterSetAllSites")] + [Parameter(ParameterSetName = ParameterSet_ALLSITES)] public SwitchParameter IncludePersonalSite { get; set; } - [Parameter(ParameterSetName = "ParameterSetPersonalSitesOnly", Mandatory = true)] + [Parameter(ParameterSetName = ParameterSet_PERSONALSITESONLY, Mandatory = true)] public SwitchParameter IncludeOnlyPersonalSite { get; set; } + [Parameter(ParameterSetName = ParameterSet_ALLSITES)] + [Parameter(ParameterSetName = ParameterSet_PERSONALSITESONLY)] + public SwitchParameter Detailed { get; set; } + protected override void ExecuteCmdlet() { bool flag = Identity != null && !string.IsNullOrEmpty(Identity.Url) && UrlUtilities.IsPersonalSiteUrl(Identity.Url); @@ -52,7 +59,7 @@ protected override void ExecuteCmdlet() } foreach (DeletedSiteProperties item in list) { - WriteObject(new Model.SPODeletedSite(item)); + WriteObject(new Model.SPODeletedSite(item, Detailed.ToBool(), AdminContext, this)); } if (!flag2 && flag3) { @@ -67,9 +74,9 @@ protected override void ExecuteCmdlet() try { AdminContext.ExecuteQueryRetry(); - WriteObject(new Model.SPODeletedSite(deletedSitePropertiesByUrl)); + WriteObject(new Model.SPODeletedSite(deletedSitePropertiesByUrl, Detailed.ToBool(), AdminContext, this)); } - catch (Microsoft.SharePoint.Client.ServerException e) when (e.ServerErrorTypeName.Equals("Microsoft.SharePoint.Client.UnknownError", StringComparison.InvariantCultureIgnoreCase)) + catch (ServerException e) when (e.ServerErrorTypeName.Equals("Microsoft.SharePoint.Client.UnknownError", StringComparison.InvariantCultureIgnoreCase)) { WriteVerbose($"No sitecollection found in the tenant recycle bin with the Url {Identity.Url}"); } diff --git a/src/Commands/Model/SPODeletedSite.cs b/src/Commands/Model/SPODeletedSite.cs index 0025bdff1..dc92bf119 100644 --- a/src/Commands/Model/SPODeletedSite.cs +++ b/src/Commands/Model/SPODeletedSite.cs @@ -1,57 +1,211 @@ using Microsoft.Online.SharePoint.TenantAdministration; +using Microsoft.SharePoint.Client; using System; +using System.Diagnostics; +using System.Management.Automation; namespace PnP.PowerShell.Commands.Model { + /// + /// Contains information about a sitecollection that is residing in the tenant recycle bin + /// public class SPODeletedSite { - private Guid siteId; + #region Basic properties - private string url; + /// + /// Unique identifier of the sitecollection + /// + public Guid SiteId { private set; get; } - private string status; + /// + /// Url of the sitecollection + /// + public string Url { get; set; } - private DateTime deletionTime; + /// + /// Status of recycling + /// + public string Status { private set; get; } - private int daysRemaining; + /// + /// Date and time at which this sitecollection was sent to the recycle bin + /// + public DateTime DeletionTime { private set; get; } - private long storageQuota; + /// + /// Amount of days remaining in the recycle bin before it will be deleted permanently + /// + public int DaysRemaining { private set; get; } - private double resourceQuota; + /// + /// The maximum amount of data that is allowed to be stored in this sitecollection + /// + public long StorageQuota { private set; get; } - public Guid SiteId => siteId; + /// + /// The sandboxed solution resource quota points assigned to this sitecollection. This is not being used anymore. + /// + public double ResourceQuota { private set; get; } - public string Url + #endregion + + #region Additional details + + // Note: These properties are only fetched if the request is made to fetch additional properties on the site + + /// + /// Date and time the sitecollection was last modified + /// + public DateTime? LastModified { private set; get; } + + /// + /// Date and time at which the site collection has been created + /// + public DateTime? CreationTime { private set; get; } + + /// + /// Date and time at which a list has last been modified within this sitecollection + /// + public DateTime? LastListActivityOn { private set; get; } + + /// + /// Date and time at which a list item has last been modified within this sitecollection + /// + public DateTime? LastItemModifiedDate { private set; get; } + + /// + /// Date and time at which there last was activity taking place on this sitecollection + /// + public DateTime? LastWebActivityOn { private set; get; } + + /// + /// Name of the user having created the sitecollection + /// + public string CreatedBy { private set; get; } + + /// + /// Name of the user having deleted the sitecollection + /// + public string DeletedBy { private set; get; } + + /// + /// Boolean indicating if this sitecollection can still be restored from the recycle bin + /// + public bool? IsRestorable { private set; get; } + + /// + /// The number of files stores within this sitecollection + /// + public long? NumberOfFiles { private set; get; } + + /// + /// The e-mail address of the primary sitecollection owner + /// + public string SiteOwnerEmail { private set; get; } + + /// + /// The name of the primary sitecollection owner + /// + public string SiteOwnerName { private set; get; } + + /// + /// The amount of SharePoint Online storage used in this sitecollection + /// + public double? StorageUsed { private set; get; } + + /// + /// The percentage of storage used towards the storage quota assigned to this sitecollection + /// + public double? StorageUsedPercentage { private set; get; } + + /// + /// The Id of the template used for creating the sitecollection + /// + public short? TemplateId { private set; get; } + + /// + /// The name of the template used for creating the sitecollection + /// + public string TemplateName { private set; get; } + + /// + /// The Id of the sensititivy label applied to this sitecollection, if any + /// + public Guid? SensitivityLabelId { private set; get; } + + /// + /// The mode for informationbarriers that is applied to this sitecollection + /// + public string InformationBarrierMode { private set; get; } + + #endregion + + /// + /// Creates a new instance based out of a instance + /// + /// Instance containing details on a deleted site coming from CSOM + /// Boolean indicating if additional details should be fetched on the deleted site + /// ClientContext that can be used to fetch the additional details. Required if is set to true, otherwise can be omitted. + /// Cmdlet instance that can be used to provide logging. Optional. + /// Thrown when is set to true but no value is provided for + internal SPODeletedSite(DeletedSiteProperties deletedSiteProperties, bool fetchAdditionalDetails = false, ClientContext clientContext = null, Cmdlet cmdlet = null) { - get + if(fetchAdditionalDetails && clientContext == null) { - return url; + throw new ArgumentNullException(nameof(clientContext), "ClientContext is required to be passed in when fetching additional details"); } - set - { - url = value; - } - } - public string Status => status; + SiteId = deletedSiteProperties.SiteId; + Url = deletedSiteProperties.Url; + DeletionTime = deletedSiteProperties.DeletionTime; + DaysRemaining = deletedSiteProperties.DaysRemaining; + Status = deletedSiteProperties.Status; + StorageQuota = deletedSiteProperties.StorageMaximumLevel; + ResourceQuota = deletedSiteProperties.UserCodeMaximumLevel; - public DateTime DeletionTime => deletionTime; + if(fetchAdditionalDetails) + { + cmdlet?.WriteVerbose($"Fetching additional details for {Url}"); + var list = clientContext.Web.Lists.GetByTitle("DO_NOT_DELETE_SPLIST_TENANTADMIN_ALL_SITES_AGGREGATED_SITECOLLECTIONS"); + CamlQuery query = new CamlQuery + { + ViewXml = $"{Url}1" + }; - public int DaysRemaining => daysRemaining; + var listItems = list.GetItems(query); + clientContext.Load(listItems); + clientContext.ExecuteQueryRetry(); - public long StorageQuota => storageQuota; + if(listItems.Count > 0) + { + cmdlet?.WriteVerbose($"Assigning additional details for {Url} to result"); + var fieldValues = listItems[0].FieldValues; - public double ResourceQuota => resourceQuota; + CreatedBy = fieldValues["CreatedBy"]?.ToString(); + DeletedBy = fieldValues["DeletedBy"]?.ToString(); + SiteOwnerEmail = fieldValues["SiteOwnerEmail"]?.ToString(); + SiteOwnerName = fieldValues["SiteOwnerName"]?.ToString(); + TemplateName = fieldValues["TemplateName"]?.ToString(); + InformationBarrierMode = fieldValues["IBMode"]?.ToString(); - internal SPODeletedSite(DeletedSiteProperties deletedSiteProperties) - { - siteId = deletedSiteProperties.SiteId; - url = deletedSiteProperties.Url; - deletionTime = deletedSiteProperties.DeletionTime; - daysRemaining = deletedSiteProperties.DaysRemaining; - status = deletedSiteProperties.Status; - storageQuota = deletedSiteProperties.StorageMaximumLevel; - resourceQuota = deletedSiteProperties.UserCodeMaximumLevel; + if (fieldValues["TimeCreated"] != null) CreationTime = DateTime.Parse(fieldValues["TimeCreated"].ToString()); + if (fieldValues["IsRestorable"] != null) IsRestorable = bool.Parse(fieldValues["IsRestorable"].ToString()); + if (fieldValues["LastListActivityOn"] != null) LastListActivityOn = DateTime.Parse(fieldValues["LastListActivityOn"].ToString()); + if (fieldValues["LastItemModifiedDate"] != null) LastItemModifiedDate = DateTime.Parse(fieldValues["LastItemModifiedDate"].ToString()); + if (fieldValues["LastItemModifiedDate"] != null) LastWebActivityOn = DateTime.Parse(fieldValues["LastItemModifiedDate"].ToString()); + if (fieldValues["NumOfFiles"] != null) NumberOfFiles = long.Parse(fieldValues["NumOfFiles"].ToString()); + if (fieldValues["StorageUsed"] != null) StorageUsed = double.Parse(fieldValues["StorageUsed"].ToString()); + if (fieldValues["TemplateId"] != null) TemplateId = short.Parse(fieldValues["TemplateId"].ToString()); + if (fieldValues["LastItemModifiedDate"] != null) LastWebActivityOn = DateTime.Parse(fieldValues["LastItemModifiedDate"].ToString()); + if (fieldValues["StorageUsedPercentage"] != null) StorageUsedPercentage = double.Parse(fieldValues["StorageUsedPercentage"].ToString()); + if (fieldValues["SensitivityLabel"] != null) SensitivityLabelId = Guid.Parse(fieldValues["SensitivityLabel"].ToString()); + } + else + { + cmdlet?.WriteVerbose($"No additional details found for {Url}"); + } + } } } } \ No newline at end of file