Skip to content

Commit 39f826c

Browse files
authored
Merge pull request #4939 from KoenZomers/PreventTemplateBeingappliedToAdminSite
Added `-Url` to `Invoke-PnPSiteTemplate` and validation not to apply it to the Admin sitecol
2 parents 8f7161d + 20ac4cf commit 39f826c

File tree

3 files changed

+112
-27
lines changed

3 files changed

+112
-27
lines changed

documentation/Invoke-PnPSiteTemplate.md

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Invoke-PnPSiteTemplate -Path <String> [-TemplateId <String>] [-ResourceFolder <S
2121
[-ProvisionFieldsToSubWebs] [-ClearNavigation] [-Parameters <Hashtable>] [-Handlers <Handlers>]
2222
[-ExcludeHandlers <Handlers>] [-ExtensibilityHandlers <ExtensibilityHandler[]>]
2323
[-TemplateProviderExtensions <ITemplateProviderExtension[]>]
24+
[-Force <boolean>] [-Url <String>]
2425
[-Connection <PnPConnection>]
2526
```
2627

@@ -31,6 +32,7 @@ Invoke-PnPSiteTemplate -InputInstance <SiteTemplate> [-TemplateId <String>] [-Re
3132
[-ProvisionFieldsToSubWebs] [-ClearNavigation] [-Parameters <Hashtable>] [-Handlers <Handlers>]
3233
[-ExcludeHandlers <Handlers>] [-ExtensibilityHandlers <ExtensibilityHandler[]>]
3334
[-TemplateProviderExtensions <ITemplateProviderExtension[]>]
35+
[-Force <boolean>] [-Url <String>]
3436
[-Connection <PnPConnection>]
3537
```
3638

@@ -41,30 +43,38 @@ Invoke-PnPSiteTemplate -Stream <Stream> [-TemplateId <String>] [-ResourceFolder
4143
[-ProvisionFieldsToSubWebs] [-ClearNavigation] [-Parameters <Hashtable>] [-Handlers <Handlers>]
4244
[-ExcludeHandlers <Handlers>] [-ExtensibilityHandlers <ExtensibilityHandler[]>]
4345
[-TemplateProviderExtensions <ITemplateProviderExtension[]>]
46+
[-Force <boolean>] [-Url <String>]
4447
[-Connection <PnPConnection>]
4548
```
4649

4750
## DESCRIPTION
4851

49-
Allows to apply a site template on a web.
52+
Allows to apply a site template on a web. The template can be in XML format or a .pnp package. The cmdlet will apply the template to the web you are currently connected to, unless you provide the -Url parameter. You can specify which parts of the template to apply by using the Handlers parameter or just omit it to apply the entire template.
5053

5154
## EXAMPLES
5255

5356
### EXAMPLE 1
5457
```powershell
55-
Invoke-PnPSiteTemplate -Path template.xml
58+
Invoke-PnPSiteTemplate -Path template.xml -Url https://tenant.sharepoint.com/sites/sitename
5659
```
5760

58-
Applies a site template in XML format to the current web.
61+
Applies a site template in XML format to the the provided site collection
5962

6063
### EXAMPLE 2
6164
```powershell
65+
Invoke-PnPSiteTemplate -Path template.xml
66+
```
67+
68+
Applies a site template in XML format to the currently connected to site
69+
70+
### EXAMPLE 3
71+
```powershell
6272
Invoke-PnPSiteTemplate -Path template.xml -ResourceFolder c:\provisioning\resources
6373
```
6474

6575
Applies a site template in XML format to the current web. Any resources like files that are referenced in the template will be retrieved from the folder as specified with the ResourceFolder parameter.
6676

67-
### EXAMPLE 3
77+
### EXAMPLE 4
6878
```powershell
6979
Invoke-PnPSiteTemplate -Path template.xml -Parameters @{"ListTitle"="Projects";"parameter2"="a second value"}
7080
```
@@ -73,28 +83,28 @@ Applies a site template in XML format to the current web. It will populate the p
7383

7484
For instance with the example above, specifying {parameter:ListTitle} in your template will translate to 'Projects' when applying the template. These tokens can be used in most string values in a template.
7585

76-
### EXAMPLE 4
86+
### EXAMPLE 5
7787
```powershell
7888
Invoke-PnPSiteTemplate -Path template.xml -Handlers Lists, SiteSecurity
7989
```
8090

8191
Applies a site template in XML format to the current web. It will only apply the lists and site security part of the template.
8292

83-
### EXAMPLE 5
93+
### EXAMPLE 6
8494
```powershell
8595
Invoke-PnPSiteTemplate -Path template.pnp
8696
```
8797

8898
Applies a site template from a pnp package to the current web.
8999

90-
### EXAMPLE 6
100+
### EXAMPLE 7
91101
```powershell
92102
Invoke-PnPSiteTemplate -Path "https://tenant.sharepoint.com/sites/templatestorage/Documents/template.pnp"
93103
```
94104

95105
Applies a site template from a pnp package stored in a library to the current web.
96106

97-
### EXAMPLE 7
107+
### EXAMPLE 8
98108
```powershell
99109
$handler1 = New-PnPExtensibilityHandlerObject -Assembly Contoso.Core.Handlers -Type Contoso.Core.Handlers.MyExtensibilityHandler1
100110
$handler2 = New-PnPExtensibilityHandlerObject -Assembly Contoso.Core.Handlers -Type Contoso.Core.Handlers.MyExtensibilityHandler2
@@ -103,21 +113,21 @@ Invoke-PnPSiteTemplate -Path NewTemplate.xml -ExtensibilityHandlers $handler1,$h
103113

104114
This will create two new ExtensibilityHandler objects that are run while provisioning the template
105115

106-
### EXAMPLE 8
116+
### EXAMPLE 9
107117
```powershell
108118
Invoke-PnPSiteTemplate -Path .\ -InputInstance $template
109119
```
110120

111121
Applies a site template from an in-memory instance of a SiteTemplate type of the PnP Core Component, reading the supporting files, if any, from the current (.\) path. The syntax can be used together with any other supported parameters.
112122

113-
### EXAMPLE 9
123+
### EXAMPLE 10
114124
```powershell
115125
Invoke-PnPSiteTemplate -Path .\template.xml -TemplateId "MyTemplate"
116126
```
117127

118128
Applies the SiteTemplate with the ID "MyTemplate" located in the template definition file template.xml.
119129

120-
### EXAMPLE 10
130+
### EXAMPLE 11
121131
```powershell
122132
$stream = Get-PnPFile -Url https://tenant.sharepoint.com/sites/TemplateGallery/Shared%20Documents/ProjectSite.pnp -AsMemoryStream
123133
Invoke-PnPSiteTemplate -Stream $stream
@@ -184,6 +194,20 @@ Accept pipeline input: False
184194
Accept wildcard characters: False
185195
```
186196
197+
### -Force
198+
When set to true, the cmdlet will not block you from applying the template to the SharePoint Online Admin Center site collection, which is not recommended. Use with care!
199+
200+
```yaml
201+
Type: Boolean
202+
Parameter Sets: (All)
203+
204+
Required: False
205+
Position: Named
206+
Default value: False
207+
Accept pipeline input: False
208+
Accept wildcard characters: False
209+
```
210+
187211
### -Handlers
188212
Allows you to only process a specific part of the template. Notice that this might fail, as some of the handlers require other artifacts in place if they are not part of what your applying. Visit https://learn.microsoft.com/dotnet/api/officedevpnp.core.framework.provisioning.model.handlers for possible values.
189213
@@ -355,7 +379,19 @@ Accept pipeline input: False
355379
Accept wildcard characters: False
356380
```
357381
382+
### -Url
383+
Optionally allows you to specify the URL of the web to apply the template to. If not specified, the template will be applied to the currently connected web. It takes precedence over the current context and requires a full URL to a web, i.e. https://tenant.sharepoint.com/sites/somesite, not just a site collection relative URL.
358384
385+
```yaml
386+
Type: string
387+
Parameter Sets: (All)
388+
389+
Required: False
390+
Position: Named
391+
Default value: None
392+
Accept pipeline input: False
393+
Accept wildcard characters: False
394+
```
359395
360396
## RELATED LINKS
361397

src/Commands/Base/PnPConnection.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,7 @@ internal string GraphEndPoint
780780
}
781781
}
782782

783-
private static bool IsTenantAdminSite(ClientRuntimeContext clientContext)
783+
public static bool IsTenantAdminSite(ClientRuntimeContext clientContext)
784784
{
785785
if (clientContext.Url.ToLower().Contains(".sharepoint."))
786786
{
@@ -805,7 +805,7 @@ private static bool IsTenantAdminSite(ClientRuntimeContext clientContext)
805805
internal void InitializeTelemetry(ClientContext context, InitializationType initializationType)
806806
{
807807
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
808-
var telemetryFile = System.IO.Path.Combine(userProfile, ".pnppowershelltelemetry");
808+
var telemetryFile = Path.Combine(userProfile, ".pnppowershelltelemetry");
809809

810810
var enableTelemetry = true;
811811
if (Environment.GetEnvironmentVariable("PNP_DISABLETELEMETRY") != null)

src/Commands/Provisioning/Site/InvokeSiteTemplate.cs

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@
1111
using PnP.Framework.Provisioning.Providers;
1212
using System.Collections.Generic;
1313
using PnP.PowerShell.Commands.Utilities;
14+
using System.Net;
15+
using PnP.PowerShell.Commands.Base;
1416

1517
namespace PnP.PowerShell.Commands.Provisioning.Site
1618
{
1719
[Cmdlet(VerbsLifecycle.Invoke, "PnPSiteTemplate")]
18-
public class InvokeSiteTemplate : PnPWebCmdlet
20+
public class InvokeSiteTemplate : PnPSharePointCmdlet
1921
{
2022
private ProgressRecord progressRecord = new ProgressRecord(0, "Activity", "Status");
2123
private ProgressRecord subProgressRecord = new ProgressRecord(1, "Activity", "Status");
2224

23-
2425
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, ValueFromPipeline = true, ParameterSetName = "Path")]
2526
public string Path;
2627

@@ -65,10 +66,58 @@ public class InvokeSiteTemplate : PnPWebCmdlet
6566

6667
[Parameter(Mandatory = false, ParameterSetName = "Stream")]
6768
public MemoryStream Stream { get; set; }
69+
70+
[Parameter(Mandatory = false)]
71+
[Alias("Url")]
72+
public string Identity { get; set; }
73+
74+
[Parameter(Mandatory = false)]
75+
public bool? Force { get; set; }
6876

6977
protected override void ExecuteCmdlet()
7078
{
71-
CurrentWeb.EnsureProperty(w => w.Url);
79+
ClientContext applyTemplateContext = null;
80+
81+
// If the Identity or Url parameter has been specified, we will build a context to apply the template to that specific site collection
82+
if (ParameterSpecified(nameof(Identity)))
83+
{
84+
// Validate if the Identity/Url parameter is a valid full URL
85+
if (Uri.TryCreate(Identity, UriKind.Absolute, out Uri uri))
86+
{
87+
LogDebug($"Connecting to the SharePoint Online site at '{uri}' to apply the template to");
88+
try
89+
{
90+
applyTemplateContext = Connection.CloneContext(Identity);
91+
}
92+
catch (WebException e) when (e.Status == WebExceptionStatus.NameResolutionFailure)
93+
{
94+
throw new PSInvalidOperationException($"The hostname '{uri}' which you have provided to apply the template to is invalid and does not exist.", e);
95+
}
96+
catch (Exception e)
97+
{
98+
throw new PSInvalidOperationException($"Unable to connect to the SharePoint Online Admin site at '{uri}' to run apply the template to. Error message: {e.Message}", e);
99+
}
100+
LogDebug($"Connected to the SharePoint Online site at '{uri}' to apply the template");
101+
}
102+
else
103+
{
104+
throw new ArgumentException("The Identity parameter, when provided, must be a valid full URL to the site collection to apply the template to.", nameof(Identity));
105+
}
106+
}
107+
else
108+
{
109+
// If the Identity/Url parameter has not been specified, we will use the current context to apply the template to
110+
applyTemplateContext = ClientContext;
111+
}
112+
113+
// Avoid the template being applied to a tenant admin site
114+
if (Force.GetValueOrDefault(false) && PnPConnection.IsTenantAdminSite(applyTemplateContext))
115+
{
116+
// If the current context is a tenant admin site, we cannot apply a site template to it
117+
throw new PSInvalidOperationException($"You cannot apply a site template to a tenant admin site. Please connect to a site collection or subsite to apply the template or use the {nameof(Identity)} parameter to specify which sitecollection it should be applied to. If you are sure you want to apply the template to a tenant admin site, please use the -{nameof(Force)} parameter to override this check.");
118+
}
119+
120+
applyTemplateContext.Web.EnsureProperty(w => w.Url);
72121
ProvisioningTemplate provisioningTemplate;
73122

74123
FileConnectorBase fileConnector;
@@ -99,8 +148,8 @@ protected override void ExecuteCmdlet()
99148
}
100149
else
101150
{
102-
Uri fileUri = new Uri(Path);
103-
var webUrl = Microsoft.SharePoint.Client.Web.WebUrlFromFolderUrlDirect(ClientContext, fileUri);
151+
Uri fileUri = new(Path);
152+
var webUrl = Web.WebUrlFromFolderUrlDirect(ClientContext, fileUri);
104153
var templateContext = ClientContext.Clone(webUrl.ToString());
105154

106155
var library = Path.ToLower().Replace(templateContext.Url.ToLower(), "").TrimStart('/');
@@ -120,7 +169,7 @@ protected override void ExecuteCmdlet()
120169
{
121170
var openXmlConnector = new OpenXMLConnector(templateFileName, fileConnector);
122171
provider = new XMLOpenXMLTemplateProvider(openXmlConnector);
123-
if (!String.IsNullOrEmpty(openXmlConnector.Info?.Properties?.TemplateFileName))
172+
if (!string.IsNullOrEmpty(openXmlConnector.Info?.Properties?.TemplateFileName))
124173
{
125174
templateFileName = openXmlConnector.Info.Properties.TemplateFileName;
126175
}
@@ -275,7 +324,7 @@ protected override void ExecuteCmdlet()
275324
{
276325
if (!ExcludeHandlers.Has(handler) && handler != Handlers.All)
277326
{
278-
Handlers = Handlers | handler;
327+
Handlers |= handler;
279328
}
280329
}
281330
applyingInformation.HandlersToProcess = Handlers;
@@ -290,8 +339,8 @@ protected override void ExecuteCmdlet()
290339
{
291340
if (message != null)
292341
{
293-
var percentage = Convert.ToInt32((100 / Convert.ToDouble(total)) * Convert.ToDouble(step));
294-
progressRecord.Activity = $"Applying template to {CurrentWeb.Url}";
342+
var percentage = Convert.ToInt32(100 / Convert.ToDouble(total) * Convert.ToDouble(step));
343+
progressRecord.Activity = $"Applying template to {applyTemplateContext.Url}";
295344
progressRecord.StatusDescription = message;
296345
progressRecord.PercentComplete = percentage;
297346
progressRecord.RecordType = ProgressRecordType.Processing;
@@ -329,7 +378,7 @@ protected override void ExecuteCmdlet()
329378
subProgressRecord.RecordType = ProgressRecordType.Processing;
330379
subProgressRecord.Activity = string.IsNullOrEmpty(messageSplitted[0]) ? "-" : messageSplitted[0];
331380
subProgressRecord.StatusDescription = string.IsNullOrEmpty(messageSplitted[1]) ? "-" : messageSplitted[1];
332-
subProgressRecord.PercentComplete = Convert.ToInt32((100 / total) * current);
381+
subProgressRecord.PercentComplete = Convert.ToInt32(100 / total * current);
333382
WriteProgress(subProgressRecord);
334383
}
335384
else
@@ -372,12 +421,12 @@ protected override void ExecuteCmdlet()
372421
return await TokenRetrieval.GetAccessTokenAsync(resource, scope, Connection);
373422
}, azureEnvironment: Connection.AzureEnvironment))
374423
{
375-
CurrentWeb.ApplyProvisioningTemplate(provisioningTemplate, applyingInformation);
424+
applyTemplateContext.Web.ApplyProvisioningTemplate(provisioningTemplate, applyingInformation);
376425
}
377426

378-
WriteProgress(new ProgressRecord(0, $"Applying template to {CurrentWeb.Url}", " ") { RecordType = ProgressRecordType.Completed });
379-
380-
if(Stream != null)
427+
WriteProgress(new ProgressRecord(0, $"Applying template to {applyTemplateContext.Url}", " ") { RecordType = ProgressRecordType.Completed });
428+
429+
if (Stream != null)
381430
{
382431
// Reset the stream position to 0 so it can be used again if needed
383432
Stream.Position = 0;

0 commit comments

Comments
 (0)