Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 47 additions & 11 deletions documentation/Invoke-PnPSiteTemplate.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Invoke-PnPSiteTemplate -Path <String> [-TemplateId <String>] [-ResourceFolder <S
[-ProvisionFieldsToSubWebs] [-ClearNavigation] [-Parameters <Hashtable>] [-Handlers <Handlers>]
[-ExcludeHandlers <Handlers>] [-ExtensibilityHandlers <ExtensibilityHandler[]>]
[-TemplateProviderExtensions <ITemplateProviderExtension[]>]
[-Force <boolean>] [-Url <String>]
[-Connection <PnPConnection>]
```

Expand All @@ -31,6 +32,7 @@ Invoke-PnPSiteTemplate -InputInstance <SiteTemplate> [-TemplateId <String>] [-Re
[-ProvisionFieldsToSubWebs] [-ClearNavigation] [-Parameters <Hashtable>] [-Handlers <Handlers>]
[-ExcludeHandlers <Handlers>] [-ExtensibilityHandlers <ExtensibilityHandler[]>]
[-TemplateProviderExtensions <ITemplateProviderExtension[]>]
[-Force <boolean>] [-Url <String>]
[-Connection <PnPConnection>]
```

Expand All @@ -41,30 +43,38 @@ Invoke-PnPSiteTemplate -Stream <Stream> [-TemplateId <String>] [-ResourceFolder
[-ProvisionFieldsToSubWebs] [-ClearNavigation] [-Parameters <Hashtable>] [-Handlers <Handlers>]
[-ExcludeHandlers <Handlers>] [-ExtensibilityHandlers <ExtensibilityHandler[]>]
[-TemplateProviderExtensions <ITemplateProviderExtension[]>]
[-Force <boolean>] [-Url <String>]
[-Connection <PnPConnection>]
```

## DESCRIPTION

Allows to apply a site template on a web.
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.

## EXAMPLES

### EXAMPLE 1
```powershell
Invoke-PnPSiteTemplate -Path template.xml
Invoke-PnPSiteTemplate -Path template.xml -Url https://tenant.sharepoint.com/sites/sitename
```

Applies a site template in XML format to the current web.
Applies a site template in XML format to the the provided site collection

### EXAMPLE 2
```powershell
Invoke-PnPSiteTemplate -Path template.xml
```

Applies a site template in XML format to the currently connected to site

### EXAMPLE 3
```powershell
Invoke-PnPSiteTemplate -Path template.xml -ResourceFolder c:\provisioning\resources
```

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.

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

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.

### EXAMPLE 4
### EXAMPLE 5
```powershell
Invoke-PnPSiteTemplate -Path template.xml -Handlers Lists, SiteSecurity
```

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

### EXAMPLE 5
### EXAMPLE 6
```powershell
Invoke-PnPSiteTemplate -Path template.pnp
```

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

### EXAMPLE 6
### EXAMPLE 7
```powershell
Invoke-PnPSiteTemplate -Path "https://tenant.sharepoint.com/sites/templatestorage/Documents/template.pnp"
```

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

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

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

### EXAMPLE 8
### EXAMPLE 9
```powershell
Invoke-PnPSiteTemplate -Path .\ -InputInstance $template
```

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.

### EXAMPLE 9
### EXAMPLE 10
```powershell
Invoke-PnPSiteTemplate -Path .\template.xml -TemplateId "MyTemplate"
```

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

### EXAMPLE 10
### EXAMPLE 11
```powershell
$stream = Get-PnPFile -Url https://tenant.sharepoint.com/sites/TemplateGallery/Shared%20Documents/ProjectSite.pnp -AsMemoryStream
Invoke-PnPSiteTemplate -Stream $stream
Expand Down Expand Up @@ -184,6 +194,20 @@ Accept pipeline input: False
Accept wildcard characters: False
```

### -Force
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!

```yaml
Type: Boolean
Parameter Sets: (All)

Required: False
Position: Named
Default value: False
Accept pipeline input: False
Accept wildcard characters: False
```

### -Handlers
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.

Expand Down Expand Up @@ -355,7 +379,19 @@ Accept pipeline input: False
Accept wildcard characters: False
```

### -Url
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.

```yaml
Type: string
Parameter Sets: (All)

Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```

## RELATED LINKS

Expand Down
4 changes: 2 additions & 2 deletions src/Commands/Base/PnPConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ internal string GraphEndPoint
}
}

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

var enableTelemetry = true;
if (Environment.GetEnvironmentVariable("PNP_DISABLETELEMETRY") != null)
Expand Down
77 changes: 63 additions & 14 deletions src/Commands/Provisioning/Site/InvokeSiteTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@
using PnP.Framework.Provisioning.Providers;
using System.Collections.Generic;
using PnP.PowerShell.Commands.Utilities;
using System.Net;
using PnP.PowerShell.Commands.Base;

namespace PnP.PowerShell.Commands.Provisioning.Site
{
[Cmdlet(VerbsLifecycle.Invoke, "PnPSiteTemplate")]
public class InvokeSiteTemplate : PnPWebCmdlet
public class InvokeSiteTemplate : PnPSharePointCmdlet
{
private ProgressRecord progressRecord = new ProgressRecord(0, "Activity", "Status");
private ProgressRecord subProgressRecord = new ProgressRecord(1, "Activity", "Status");


[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true, ValueFromPipeline = true, ParameterSetName = "Path")]
public string Path;

Expand Down Expand Up @@ -65,10 +66,58 @@ public class InvokeSiteTemplate : PnPWebCmdlet

[Parameter(Mandatory = false, ParameterSetName = "Stream")]
public MemoryStream Stream { get; set; }

[Parameter(Mandatory = false)]
[Alias("Url")]
public string Identity { get; set; }

[Parameter(Mandatory = false)]
public bool? Force { get; set; }

protected override void ExecuteCmdlet()
{
CurrentWeb.EnsureProperty(w => w.Url);
ClientContext applyTemplateContext = null;

// If the Identity or Url parameter has been specified, we will build a context to apply the template to that specific site collection
if (ParameterSpecified(nameof(Identity)))
{
// Validate if the Identity/Url parameter is a valid full URL
if (Uri.TryCreate(Identity, UriKind.Absolute, out Uri uri))
{
LogDebug($"Connecting to the SharePoint Online site at '{uri}' to apply the template to");
try
{
applyTemplateContext = Connection.CloneContext(Identity);
}
catch (WebException e) when (e.Status == WebExceptionStatus.NameResolutionFailure)
{
throw new PSInvalidOperationException($"The hostname '{uri}' which you have provided to apply the template to is invalid and does not exist.", e);
}
catch (Exception e)
{
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);
}
LogDebug($"Connected to the SharePoint Online site at '{uri}' to apply the template");
}
else
{
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));
}
}
else
{
// If the Identity/Url parameter has not been specified, we will use the current context to apply the template to
applyTemplateContext = ClientContext;
}

// Avoid the template being applied to a tenant admin site
if (Force.GetValueOrDefault(false) && PnPConnection.IsTenantAdminSite(applyTemplateContext))
{
// If the current context is a tenant admin site, we cannot apply a site template to it
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.");
}

applyTemplateContext.Web.EnsureProperty(w => w.Url);
ProvisioningTemplate provisioningTemplate;

FileConnectorBase fileConnector;
Expand Down Expand Up @@ -99,8 +148,8 @@ protected override void ExecuteCmdlet()
}
else
{
Uri fileUri = new Uri(Path);
var webUrl = Microsoft.SharePoint.Client.Web.WebUrlFromFolderUrlDirect(ClientContext, fileUri);
Uri fileUri = new(Path);
var webUrl = Web.WebUrlFromFolderUrlDirect(ClientContext, fileUri);
var templateContext = ClientContext.Clone(webUrl.ToString());

var library = Path.ToLower().Replace(templateContext.Url.ToLower(), "").TrimStart('/');
Expand All @@ -120,7 +169,7 @@ protected override void ExecuteCmdlet()
{
var openXmlConnector = new OpenXMLConnector(templateFileName, fileConnector);
provider = new XMLOpenXMLTemplateProvider(openXmlConnector);
if (!String.IsNullOrEmpty(openXmlConnector.Info?.Properties?.TemplateFileName))
if (!string.IsNullOrEmpty(openXmlConnector.Info?.Properties?.TemplateFileName))
{
templateFileName = openXmlConnector.Info.Properties.TemplateFileName;
}
Expand Down Expand Up @@ -275,7 +324,7 @@ protected override void ExecuteCmdlet()
{
if (!ExcludeHandlers.Has(handler) && handler != Handlers.All)
{
Handlers = Handlers | handler;
Handlers |= handler;
}
}
applyingInformation.HandlersToProcess = Handlers;
Expand All @@ -290,8 +339,8 @@ protected override void ExecuteCmdlet()
{
if (message != null)
{
var percentage = Convert.ToInt32((100 / Convert.ToDouble(total)) * Convert.ToDouble(step));
progressRecord.Activity = $"Applying template to {CurrentWeb.Url}";
var percentage = Convert.ToInt32(100 / Convert.ToDouble(total) * Convert.ToDouble(step));
progressRecord.Activity = $"Applying template to {applyTemplateContext.Url}";
progressRecord.StatusDescription = message;
progressRecord.PercentComplete = percentage;
progressRecord.RecordType = ProgressRecordType.Processing;
Expand Down Expand Up @@ -329,7 +378,7 @@ protected override void ExecuteCmdlet()
subProgressRecord.RecordType = ProgressRecordType.Processing;
subProgressRecord.Activity = string.IsNullOrEmpty(messageSplitted[0]) ? "-" : messageSplitted[0];
subProgressRecord.StatusDescription = string.IsNullOrEmpty(messageSplitted[1]) ? "-" : messageSplitted[1];
subProgressRecord.PercentComplete = Convert.ToInt32((100 / total) * current);
subProgressRecord.PercentComplete = Convert.ToInt32(100 / total * current);
WriteProgress(subProgressRecord);
}
else
Expand Down Expand Up @@ -372,12 +421,12 @@ protected override void ExecuteCmdlet()
return await TokenRetrieval.GetAccessTokenAsync(resource, scope, Connection);
}, azureEnvironment: Connection.AzureEnvironment))
{
CurrentWeb.ApplyProvisioningTemplate(provisioningTemplate, applyingInformation);
applyTemplateContext.Web.ApplyProvisioningTemplate(provisioningTemplate, applyingInformation);
}

WriteProgress(new ProgressRecord(0, $"Applying template to {CurrentWeb.Url}", " ") { RecordType = ProgressRecordType.Completed });
if(Stream != null)
WriteProgress(new ProgressRecord(0, $"Applying template to {applyTemplateContext.Url}", " ") { RecordType = ProgressRecordType.Completed });

if (Stream != null)
{
// Reset the stream position to 0 so it can be used again if needed
Stream.Position = 0;
Expand Down
Loading