From 06b70877d676b983b634b65b94ba212aaa529325 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Mon, 7 Feb 2022 07:55:43 +0200 Subject: [PATCH 1/3] Feature #603 - added cmdlet to rename site URL --- CHANGELOG.md | 1 + documentation/Rename-PnPTenantSite.md | 157 ++++++++++++++++++++++++ src/Commands/Admin/RenameTenantSite.cs | 162 +++++++++++++++++++++++++ src/Commands/Model/SPOSiteRename.cs | 37 ++++++ 4 files changed, 357 insertions(+) create mode 100644 documentation/Rename-PnPTenantSite.md create mode 100644 src/Commands/Admin/RenameTenantSite.cs create mode 100644 src/Commands/Model/SPOSiteRename.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe1b2413..405c3fffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added `Publish\Unpublish-PnPContentType` to allow for content types to be published or unpublished on hub sites [#1597](https://github.com/pnp/powershell/pull/1597) - Added `Get-PnPContentTypePublishingStatus` to get te current publication state of a content type in the content type hub site [#1597](https://github.com/pnp/powershell/pull/1597) - Added ability to pipe the output of `Get-PnPTenantDeletedSite` to either `Restore-PnPTenantDeletedSite` or `Remove-PnPTenantDeletedSite` [#1596](https://github.com/pnp/powershell/pull/1596) +- Added `Rename-PnPTenantSite` to rename a SharePoint Online site URL. ### Changed diff --git a/documentation/Rename-PnPTenantSite.md b/documentation/Rename-PnPTenantSite.md new file mode 100644 index 000000000..f02ba4e9a --- /dev/null +++ b/documentation/Rename-PnPTenantSite.md @@ -0,0 +1,157 @@ +--- +Module Name: PnP.PowerShell +title: Rename-PnPTenantSite +schema: 2.0.0 +applicable: SharePoint Online +external help file: PnP.PowerShell.dll-Help.xml +online version: https://pnp.github.io/powershell/cmdlets/Rename-PnPTenantSite.html +--- + +# Rename-PnPTenantSite + +## SYNOPSIS +This command starts a rename of a site on a SharePoint Online site. You can change the URL, and optionally the site title along with changing the URL. + +This will not work for Multi-geo environments. + +## SYNTAX + +```powershell +Rename-PnPTenantSite [[-Identity] ] [[-NewSiteUrl] ] [[-NewSiteTitle] ] +[[-SuppressMarketplaceAppCheck] []] [[-SuppressWorkflow2013Check] []] [-Connection ] +[] +``` + +## DESCRIPTION + +## EXAMPLES + +### EXAMPLE 1 +```powershell +$url="https://.sharepoint.com/site/samplesite" +$NewSiteUrl="https://.sharepoint.com/site/renamed" +Rename-PnPTenantSite -Identity $url -NewSiteUrl $NewSiteUrl +``` + +Starts the rename of the SPO site with name "samplesite" to "renamed" without modifying the title. + +## PARAMETERS + +### -Connection +Optional connection to be used by the cmdlet. Retrieve the value for this parameter by either specifying -ReturnConnection on Connect-PnPOnline or by executing Get-PnPConnection. + +```yaml +Type: PnPConnection +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Identity +Specifies the URL of the SharePoint Site on which SharePoint Spaces should be disabled. Must be provided if Scope is set to Tenant. + +```yaml +Type: SPOSitePipeBind +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: True +Accept wildcard characters: False +``` + +### -NewSiteUrl +Specifies the new URL of the SharePoint Site that you want to set + +```yaml +Type: String +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: True +Accept wildcard characters: False +``` + +### -NewSiteTitle +Specifies the new title of of the SharePoint Site. + +```yaml +Type: String +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: True +Accept wildcard characters: False +``` + +### -SuppressMarketplaceAppCheck +Suppress checking compatibility of marketplace SharePoint Add-ins deployed to the associated site. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: True +Accept wildcard characters: False +``` + +### -SuppressWorkflow2013Check +Suppress checking compatibility of SharePoint 2013 Workflows deployed to the associated site. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: True +Accept wildcard characters: False +``` + +### -SuppressBcsCheck +Suppress checking compatibility of BCS connections deployed to the associated site. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: True +Accept wildcard characters: False +``` + +### -Wait +Wait till the renaming of the new site collection is successfull. If not specified, a job will be created for SPO and you can check it a few seconds or minutes later. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: True +Accept wildcard characters: False +``` + + + +## RELATED LINKS + +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) + diff --git a/src/Commands/Admin/RenameTenantSite.cs b/src/Commands/Admin/RenameTenantSite.cs new file mode 100644 index 000000000..8700c33fe --- /dev/null +++ b/src/Commands/Admin/RenameTenantSite.cs @@ -0,0 +1,162 @@ +using Microsoft.SharePoint.Client; +using PnP.Framework.Http; +using PnP.PowerShell.Commands.Base; +using PnP.PowerShell.Commands.Base.PipeBinds; +using PnP.PowerShell.Commands.Model; +using PnP.PowerShell.Commands.Utilities; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace PnP.PowerShell.Commands.Admin +{ + [Cmdlet(VerbsCommon.Rename, "PnPTenantSite")] + public class RenameTenantSite : PnPAdminCmdlet + { + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public SPOSitePipeBind Identity { get; set; } + + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string NewSiteUrl { get; set; } + + [Parameter(Mandatory = false)] + [ValidateNotNullOrEmpty] + public string NewSiteTitle { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter SuppressMarketplaceAppCheck { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter SuppressWorkflow2013Check { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter SuppressBcsCheck { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter Wait { get; set; } + + protected override void ExecuteCmdlet() + { + ClientContext.ExecuteQueryRetry(); // fixes issue where ServerLibraryVersion is not available. + + int optionsBitMask = 0; + if (SuppressMarketplaceAppCheck.IsPresent) + { + optionsBitMask |= 8; + } + if (SuppressWorkflow2013Check.IsPresent) + { + optionsBitMask |= 16; + } + if (SuppressBcsCheck.IsPresent) + { + optionsBitMask |= 128; + } + + var body = new + { + SourceSiteUrl = Identity.Url, + TargetSiteUrl = NewSiteUrl, + TargetSiteTitle = NewSiteTitle ?? null, + Option = optionsBitMask, + Reserve = string.Empty, + OperationId = Guid.Empty + }; + + var tenantUrl = UrlUtilities.GetTenantAdministrationUrl(ClientContext.Url); + try + { + var results = Utilities.REST.RestHelper.PostAsync(HttpClient, $"{tenantUrl.TrimEnd('/')}/_api/SiteRenameJobs?api-version=1.4.7", ClientContext, body, false).GetAwaiter().GetResult(); + if (!Wait.IsPresent) + { + if (results != null) + { + WriteObject(results); + } + } + else + { + bool wait = true; + var iterations = 0; + + var method = new HttpMethod("GET"); + + var httpClient = PnPHttpClient.Instance.GetHttpClient(ClientContext); + + var requestUrl = $"{tenantUrl.TrimEnd('/')}/_api/SiteRenameJobs/GetJobsBySiteUrl(url='{Identity.Url}')?api-version=1.4.7"; + + while (wait) + { + iterations++; + try + { + using (HttpRequestMessage request = new HttpRequestMessage(method, requestUrl)) + { + request.Headers.Add("accept", "application/json;odata=nometadata"); + request.Headers.Add("X-AttemptNumber", iterations.ToString()); + PnPHttpClient.AuthenticateRequestAsync(request, ClientContext).GetAwaiter().GetResult(); + + HttpResponseMessage response = httpClient.SendAsync(request, new System.Threading.CancellationToken()).Result; + + if (response.IsSuccessStatusCode) + { + var responseString = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + if (responseString != null) + { + var jsonElement = JsonSerializer.Deserialize(responseString); + + if (jsonElement.TryGetProperty("value", out JsonElement valueProperty)) + { + var siteRenameResults = JsonSerializer.Deserialize>(valueProperty.ToString()); + + if (siteRenameResults != null && siteRenameResults.Count > 0) + { + var siteRenameResponse = siteRenameResults[0]; + if (!string.IsNullOrEmpty(siteRenameResponse.ErrorDescription)) + { + wait = false; + throw new PSInvalidOperationException(siteRenameResponse.ErrorDescription); + } + if (siteRenameResponse.JobState == "Success") + { + wait = false; + WriteObject(siteRenameResponse); + } + else + { + Task.Delay(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult(); + } + } + } + } + } + } + } + catch (Exception) + { + if (iterations * 30 >= 300) + { + wait = false; + throw; + } + else + { + Task.Delay(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult(); + } + } + } + } + } + catch + { + throw; + } + } + } +} diff --git a/src/Commands/Model/SPOSiteRename.cs b/src/Commands/Model/SPOSiteRename.cs new file mode 100644 index 000000000..f2175c21a --- /dev/null +++ b/src/Commands/Model/SPOSiteRename.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace PnP.PowerShell.Commands.Model +{ + public class SPOSiteRenameJob + { + public string JobState { get; set; } + + public Guid SiteId { get; set; } + + public Guid JobId { get; set; } + + public Guid ParentId { get; set; } + + public string TriggeredBy { get; set; } + + public int ErrorCode { get; set; } + + public string ErrorDescription { get; set; } + + public string SourceSiteUrl { get; set; } + + public string TargetSiteUrl { get; set; } + + public object TargetSiteTitle { get; set; } + + public int Option { get; set; } + + public object Reserve { get; set; } + + public object SkipGestures { get; set; } + + } +} From 4b06b83b8103cbd3fd8bbc0c291be1f811cf102c Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Tue, 8 Feb 2022 14:55:33 +0100 Subject: [PATCH 2/3] Adding PR reference --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 405c3fffe..e10317a67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added `Publish\Unpublish-PnPContentType` to allow for content types to be published or unpublished on hub sites [#1597](https://github.com/pnp/powershell/pull/1597) - Added `Get-PnPContentTypePublishingStatus` to get te current publication state of a content type in the content type hub site [#1597](https://github.com/pnp/powershell/pull/1597) - Added ability to pipe the output of `Get-PnPTenantDeletedSite` to either `Restore-PnPTenantDeletedSite` or `Remove-PnPTenantDeletedSite` [#1596](https://github.com/pnp/powershell/pull/1596) -- Added `Rename-PnPTenantSite` to rename a SharePoint Online site URL. +- Added `Rename-PnPTenantSite` to rename a SharePoint Online site URL [#1606](https://github.com/pnp/powershell/pull/1606) ### Changed From 36bea2d566046274ff20c5d0f821278ad857371e Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Tue, 8 Feb 2022 15:18:08 +0100 Subject: [PATCH 3/3] Some cleanup --- documentation/Rename-PnPTenantSite.md | 32 +++---- src/Commands/Admin/RenameTenantSite.cs | 117 ++++++++++++------------- src/Commands/Model/SPOSiteRename.cs | 58 +++++++++--- 3 files changed, 117 insertions(+), 90 deletions(-) diff --git a/documentation/Rename-PnPTenantSite.md b/documentation/Rename-PnPTenantSite.md index f02ba4e9a..ec8b30407 100644 --- a/documentation/Rename-PnPTenantSite.md +++ b/documentation/Rename-PnPTenantSite.md @@ -18,8 +18,7 @@ This will not work for Multi-geo environments. ```powershell Rename-PnPTenantSite [[-Identity] ] [[-NewSiteUrl] ] [[-NewSiteTitle] ] -[[-SuppressMarketplaceAppCheck] []] [[-SuppressWorkflow2013Check] []] [-Connection ] -[] +[[-SuppressMarketplaceAppCheck] []] [[-SuppressWorkflow2013Check] []] [-Connection ] [] ``` ## DESCRIPTION @@ -28,12 +27,12 @@ Rename-PnPTenantSite [[-Identity] ] [[-NewSiteUrl] ] [[ ### EXAMPLE 1 ```powershell -$url="https://.sharepoint.com/site/samplesite" -$NewSiteUrl="https://.sharepoint.com/site/renamed" -Rename-PnPTenantSite -Identity $url -NewSiteUrl $NewSiteUrl +$currentSiteUrl = "https://.sharepoint.com/site/samplesite" +$updatedSiteUrl = "https://.sharepoint.com/site/renamed" +Rename-PnPTenantSite -Identity $currentSiteUrl -NewSiteUrl $updatedSiteUrl ``` -Starts the rename of the SPO site with name "samplesite" to "renamed" without modifying the title. +Starts the rename of the SharePoint Online site with name "samplesite" to "renamed" without modifying the title. ## PARAMETERS @@ -52,7 +51,7 @@ Accept wildcard characters: False ``` ### -Identity -Specifies the URL of the SharePoint Site on which SharePoint Spaces should be disabled. Must be provided if Scope is set to Tenant. +Specifies the full URL of the SharePoint Online site collection that needs to be renamed. ```yaml Type: SPOSitePipeBind @@ -66,7 +65,7 @@ Accept wildcard characters: False ``` ### -NewSiteUrl -Specifies the new URL of the SharePoint Site that you want to set +Specifies the full URL of the SharePoint Online site collection to which it needs to be renamed. ```yaml Type: String @@ -75,7 +74,7 @@ Parameter Sets: (All) Required: False Position: Named Default value: None -Accept pipeline input: True +Accept pipeline input: False Accept wildcard characters: False ``` @@ -89,7 +88,7 @@ Parameter Sets: (All) Required: False Position: Named Default value: None -Accept pipeline input: True +Accept pipeline input: False Accept wildcard characters: False ``` @@ -103,7 +102,7 @@ Parameter Sets: (All) Required: False Position: Named Default value: None -Accept pipeline input: True +Accept pipeline input: False Accept wildcard characters: False ``` @@ -117,7 +116,7 @@ Parameter Sets: (All) Required: False Position: Named Default value: None -Accept pipeline input: True +Accept pipeline input: False Accept wildcard characters: False ``` @@ -136,7 +135,7 @@ Accept wildcard characters: False ``` ### -Wait -Wait till the renaming of the new site collection is successfull. If not specified, a job will be created for SPO and you can check it a few seconds or minutes later. +Wait till the renaming of the new site collection is successfull. If not specified, a job will be created which you can use to check for its status. ```yaml Type: SwitchParameter @@ -145,13 +144,10 @@ Parameter Sets: (All) Required: False Position: Named Default value: None -Accept pipeline input: True +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/RenameTenantSite.cs b/src/Commands/Admin/RenameTenantSite.cs index 8700c33fe..2df6aac20 100644 --- a/src/Commands/Admin/RenameTenantSite.cs +++ b/src/Commands/Admin/RenameTenantSite.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Management.Automation; using System.Net.Http; -using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -70,93 +69,87 @@ protected override void ExecuteCmdlet() }; var tenantUrl = UrlUtilities.GetTenantAdministrationUrl(ClientContext.Url); - try + + var results = Utilities.REST.RestHelper.PostAsync(HttpClient, $"{tenantUrl.TrimEnd('/')}/_api/SiteRenameJobs?api-version=1.4.7", ClientContext, body, false).GetAwaiter().GetResult(); + if (!Wait.IsPresent) { - var results = Utilities.REST.RestHelper.PostAsync(HttpClient, $"{tenantUrl.TrimEnd('/')}/_api/SiteRenameJobs?api-version=1.4.7", ClientContext, body, false).GetAwaiter().GetResult(); - if (!Wait.IsPresent) + if (results != null) { - if (results != null) - { - WriteObject(results); - } + WriteObject(results); } - else - { - bool wait = true; - var iterations = 0; + } + else + { + bool wait = true; + var iterations = 0; - var method = new HttpMethod("GET"); + var method = new HttpMethod("GET"); - var httpClient = PnPHttpClient.Instance.GetHttpClient(ClientContext); + var httpClient = PnPHttpClient.Instance.GetHttpClient(ClientContext); - var requestUrl = $"{tenantUrl.TrimEnd('/')}/_api/SiteRenameJobs/GetJobsBySiteUrl(url='{Identity.Url}')?api-version=1.4.7"; + var requestUrl = $"{tenantUrl.TrimEnd('/')}/_api/SiteRenameJobs/GetJobsBySiteUrl(url='{Identity.Url}')?api-version=1.4.7"; - while (wait) + while (wait) + { + iterations++; + try { - iterations++; - try + using (HttpRequestMessage request = new HttpRequestMessage(method, requestUrl)) { - using (HttpRequestMessage request = new HttpRequestMessage(method, requestUrl)) - { - request.Headers.Add("accept", "application/json;odata=nometadata"); - request.Headers.Add("X-AttemptNumber", iterations.ToString()); - PnPHttpClient.AuthenticateRequestAsync(request, ClientContext).GetAwaiter().GetResult(); + request.Headers.Add("accept", "application/json;odata=nometadata"); + request.Headers.Add("X-AttemptNumber", iterations.ToString()); + PnPHttpClient.AuthenticateRequestAsync(request, ClientContext).GetAwaiter().GetResult(); - HttpResponseMessage response = httpClient.SendAsync(request, new System.Threading.CancellationToken()).Result; + HttpResponseMessage response = httpClient.SendAsync(request, new System.Threading.CancellationToken()).Result; - if (response.IsSuccessStatusCode) + if (response.IsSuccessStatusCode) + { + var responseString = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + if (responseString != null) { - var responseString = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - if (responseString != null) + var jsonElement = JsonSerializer.Deserialize(responseString); + + if (jsonElement.TryGetProperty("value", out JsonElement valueProperty)) { - var jsonElement = JsonSerializer.Deserialize(responseString); + var siteRenameResults = JsonSerializer.Deserialize>(valueProperty.ToString()); - if (jsonElement.TryGetProperty("value", out JsonElement valueProperty)) + if (siteRenameResults != null && siteRenameResults.Count > 0) { - var siteRenameResults = JsonSerializer.Deserialize>(valueProperty.ToString()); - - if (siteRenameResults != null && siteRenameResults.Count > 0) + var siteRenameResponse = siteRenameResults[0]; + if (!string.IsNullOrEmpty(siteRenameResponse.ErrorDescription)) + { + wait = false; + throw new PSInvalidOperationException(siteRenameResponse.ErrorDescription); + } + if (siteRenameResponse.JobState == "Success") { - var siteRenameResponse = siteRenameResults[0]; - if (!string.IsNullOrEmpty(siteRenameResponse.ErrorDescription)) - { - wait = false; - throw new PSInvalidOperationException(siteRenameResponse.ErrorDescription); - } - if (siteRenameResponse.JobState == "Success") - { - wait = false; - WriteObject(siteRenameResponse); - } - else - { - Task.Delay(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult(); - } + wait = false; + WriteObject(siteRenameResponse); + } + else + { + Task.Delay(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult(); } } } } } } - catch (Exception) + } + catch (Exception) + { + if (iterations * 30 >= 300) { - if (iterations * 30 >= 300) - { - wait = false; - throw; - } - else - { - Task.Delay(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult(); - } + wait = false; + throw; + } + else + { + Task.Delay(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult(); } } } } - catch - { - throw; - } } } -} +} \ No newline at end of file diff --git a/src/Commands/Model/SPOSiteRename.cs b/src/Commands/Model/SPOSiteRename.cs index f2175c21a..c60eb7837 100644 --- a/src/Commands/Model/SPOSiteRename.cs +++ b/src/Commands/Model/SPOSiteRename.cs @@ -1,37 +1,75 @@ using System; -using System.Collections.Generic; -using System.Text; -using System.Text.Json.Serialization; namespace PnP.PowerShell.Commands.Model { + /// + /// Contains information about an ongoing SharePoint Online site collection rename + /// public class SPOSiteRenameJob { + /// + /// State the rename process is in + /// public string JobState { get; set; } - public Guid SiteId { get; set; } + /// + /// Id of the site that is being renamed + /// + public Guid? SiteId { get; set; } - public Guid JobId { get; set; } + /// + /// Unique identifier of the rename job + /// + public Guid? JobId { get; set; } - public Guid ParentId { get; set; } + /// + /// Unknown + /// + public Guid? ParentId { get; set; } + /// + /// Person or process having initiated the rename + /// public string TriggeredBy { get; set; } - public int ErrorCode { get; set; } + /// + /// Error code, if any + /// + public int? ErrorCode { get; set; } + /// + /// Error description, if any + /// public string ErrorDescription { get; set; } + /// + /// Url of the site collection before the rename + /// public string SourceSiteUrl { get; set; } + /// + /// Url of the site collection after the rename + /// public string TargetSiteUrl { get; set; } + /// + /// Unknown + /// public object TargetSiteTitle { get; set; } - public int Option { get; set; } + /// + /// Unknown + /// + public int? Option { get; set; } + /// + /// Unknown + /// public object Reserve { get; set; } + /// + /// Unknown + /// public object SkipGestures { get; set; } - } -} +} \ No newline at end of file